Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,29 @@ KWAVE_MATLAB_PATH = $(abspath ../k-wave) # Get absolute path of k-wave directory

# Define the artifact directory
COLLECTED_VALUES_DIR = $(abspath tests/matlab_test_data_collectors/python_testers/collectedValues)
EXAMPLE_OUTPUT_DIR = /tmp/example_runs
COMPARE_OUTPUT_DIR_A ?= /tmp/example_runs
COMPARE_OUTPUT_DIR_B ?= /tmp/example_runs_1

# Default target
all: run-examples test

# Target to run all examples
run-examples:
@echo "Running all examples..."
@MPLBACKEND=Agg $(PYTHON) run_examples.py
@PYTHONPATH="$(CURDIR):$$PYTHONPATH" MPLBACKEND=Agg $(PYTHON) run_examples.py

# Target to run all examples and collect non-dated HDF5 inputs/outputs
run-examples-no-dates:
@echo "Running all examples with non-dated HDF5 filenames..."
@rm -rf $(EXAMPLE_OUTPUT_DIR)
@PYTHONPATH="$(CURDIR):$$PYTHONPATH" KWAVE_USE_DATED_FILENAMES=0 MPLBACKEND=Agg $(PYTHON) run_examples.py
@echo "Collected example files in $(EXAMPLE_OUTPUT_DIR)"

# Target to compare two example output roots
compare-example-outputs:
@echo "Comparing HDF outputs: $(COMPARE_OUTPUT_DIR_A) vs $(COMPARE_OUTPUT_DIR_B)"
@PYTHONPATH="$(CURDIR):$$PYTHONPATH" $(PYTHON) scripts/compare_example_outputs.py $(COMPARE_OUTPUT_DIR_A) $(COMPARE_OUTPUT_DIR_B)

# Target to run pytest, which depends on running the MATLAB script first
test: $(COLLECTED_VALUES_DIR)
Expand All @@ -41,4 +56,4 @@ clean-collected_values:
@echo "Cleaning collected values directory..."
@rm -rf $(COLLECTED_VALUES_DIR)

.PHONY: all run-examples run-tests clean-python clean-collected_values clean
.PHONY: all run-examples run-examples-no-dates compare-example-outputs run-tests clean-python clean-collected_values clean
59 changes: 40 additions & 19 deletions examples/checkpointing/checkpoint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from copy import copy
from pathlib import Path
from tempfile import TemporaryDirectory
from shutil import copy2

import numpy as np

Expand All @@ -15,6 +14,8 @@
from kwave.utils.filters import smooth
from kwave.utils.mapgen import make_ball

EXAMPLE_OUTPUT_DIR = Path("/tmp/example_runs/checkpointing")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded /tmp path is not portable to Windows.

Path("/tmp/...") won't work on Windows. Consider using tempfile.gettempdir() or Path(tempfile.gettempdir()) for cross-platform compatibility.

Proposed fix
+import tempfile
+
-EXAMPLE_OUTPUT_DIR = Path("/tmp/example_runs/checkpointing")
+EXAMPLE_OUTPUT_DIR = Path(tempfile.gettempdir()) / "example_runs" / "checkpointing"
🤖 Prompt for AI Agents
In `@examples/checkpointing/checkpoint.py` at line 17, EXAMPLE_OUTPUT_DIR is
hardcoded to a Unix-only "/tmp" path; replace its construction to use the system
temporary directory (e.g., Path(tempfile.gettempdir())) so the code works on
Windows and other platforms—update the definition of EXAMPLE_OUTPUT_DIR in
checkpoint.py to build the path from tempfile.gettempdir() (import tempfile if
not already).


# This script demonstrates how to use the checkpointing feature of k-Wave.
# It runs the same simulation twice, with the second run starting from a
# checkpoint file created during the first run.
Expand All @@ -30,6 +31,17 @@
# Note: checkpoint timesteps and checkpoint interval must be integers.


def _next_available_path(path: Path) -> Path:
if not path.exists():
return path
suffix = 1
while True:
candidate = path.with_name(f"{path.stem}_{suffix}{path.suffix}")
if not candidate.exists():
return candidate
suffix += 1


def make_simulation_parameters(directory: Path, checkpoint_timesteps: int):
"""
See the 3D FFT Reconstruction For A Planar Sensor example for context.
Expand Down Expand Up @@ -71,6 +83,7 @@ def make_simulation_parameters(directory: Path, checkpoint_timesteps: int):
pml_inside=False,
smooth_p0=False,
data_cast="single",
allow_file_overwrite=True,
input_filename=input_filename,
output_filename=output_filename,
)
Expand All @@ -84,23 +97,31 @@ def main():
# the halfway point.
checkpoint_timesteps = 106

with TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
# create the simulation parameters for the 1st run
kgrid, medium, source, sensor, simulation_options, execution_options = make_simulation_parameters(
directory=tmpdir, checkpoint_timesteps=checkpoint_timesteps
)
kspaceFirstOrder3D(kgrid, source, sensor, medium, simulation_options, execution_options)
print("Temporary directory contains 3 files (including checkpoint):")
print("\t-", "\n\t- ".join([f.name for f in tmpdir.glob("*.h5")]))

# create the simulation parameters for the 2nd run
kgrid, medium, source, sensor, simulation_options, execution_options = make_simulation_parameters(
directory=tmpdir, checkpoint_timesteps=checkpoint_timesteps
)
kspaceFirstOrder3D(kgrid, source, sensor, medium, simulation_options, execution_options)
print("Temporary directory contains 2 files (checkpoint has been deleted):")
print("\t-", "\n\t- ".join([f.name for f in tmpdir.glob("*.h5")]))
EXAMPLE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
for filename in ("kwave_input.h5", "kwave_output.h5", "kwave_checkpoint.h5"):
(EXAMPLE_OUTPUT_DIR / filename).unlink(missing_ok=True)

# create the simulation parameters for the 1st run
kgrid, medium, source, sensor, simulation_options, execution_options = make_simulation_parameters(
directory=EXAMPLE_OUTPUT_DIR,
checkpoint_timesteps=checkpoint_timesteps,
)
kspaceFirstOrder3D(kgrid, source, sensor, medium, simulation_options, execution_options)
copy2(simulation_options.input_filename, _next_available_path(EXAMPLE_OUTPUT_DIR / "run_1_kwave_input.h5"))
copy2(simulation_options.output_filename, _next_available_path(EXAMPLE_OUTPUT_DIR / "run_1_kwave_output.h5"))
print("Checkpoint output directory after run_1:")
print("\t-", "\n\t- ".join([f.name for f in EXAMPLE_OUTPUT_DIR.glob("*.h5")]))

# create the simulation parameters for the 2nd run
kgrid, medium, source, sensor, simulation_options, execution_options = make_simulation_parameters(
directory=EXAMPLE_OUTPUT_DIR,
checkpoint_timesteps=checkpoint_timesteps,
)
kspaceFirstOrder3D(kgrid, source, sensor, medium, simulation_options, execution_options)
copy2(simulation_options.input_filename, _next_available_path(EXAMPLE_OUTPUT_DIR / "run_2_kwave_input.h5"))
copy2(simulation_options.output_filename, _next_available_path(EXAMPLE_OUTPUT_DIR / "run_2_kwave_output.h5"))
print("Checkpoint output directory after run_2:")
print("\t-", "\n\t- ".join([f.name for f in EXAMPLE_OUTPUT_DIR.glob("*.h5")]))


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import os
from copy import deepcopy
from tempfile import gettempdir

import matplotlib.pyplot as plt
import numpy as np
Expand Down Expand Up @@ -73,9 +71,8 @@
# run the simulation

input_filename = f"example_input_{source_loop + 1}_input.h5"
pathname = gettempdir()
input_file_full_path = os.path.join(pathname, input_filename)
simulation_options = SimulationOptions(save_to_disk=True, input_filename=input_filename, data_path=pathname)
output_filename = f"example_output_{source_loop + 1}_output.h5"
simulation_options = SimulationOptions(save_to_disk=True, input_filename=input_filename, output_filename=output_filename)
# run the simulation
sensor_data = kspaceFirstOrder2DC(
medium=medium,
Expand Down
6 changes: 1 addition & 5 deletions examples/sd_focussed_detector_2D/sd_focussed_detector_2D.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# # Focussed Detector In 2D Example
# This example shows how k-Wave-python can be used to model the output of a focused semicircular detector, where the directionality arises from spatially averaging across the detector surface. Unlike the original example in k-Wave, this example does not visualize the simulation, as this functionality is not intrinsically supported by the accelerated binaries.

import os
from copy import deepcopy
from tempfile import gettempdir

import matplotlib.pyplot as plt
import numpy as np
Expand Down Expand Up @@ -43,9 +41,7 @@

# ## Define simulation parameters
input_filename = "example_sd_focused_2d_input.h5"
pathname = gettempdir()
input_file_full_path = os.path.join(pathname, input_filename)
simulation_options = SimulationOptions(save_to_disk=True, input_filename=input_filename, data_path=pathname)
simulation_options = SimulationOptions(save_to_disk=True, input_filename=input_filename)


# ## Run simulation with first source
Expand Down
6 changes: 1 addition & 5 deletions examples/sd_focussed_detector_3D/sd_focussed_detector_3D.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# Focussed Detector In 3D Example
# This example shows how k-Wave can be used to model the output of a focussed bowl detector where the directionality arises from spatially averaging across the detector surface.

import os
from copy import deepcopy
from tempfile import gettempdir

import matplotlib.pyplot as plt
import numpy as np
Expand Down Expand Up @@ -33,10 +31,8 @@
_ = kgrid.makeTime(medium.sound_speed)

input_filename = "example_sd_focused_3d_input.h5"
pathname = gettempdir()
input_file_full_path = os.path.join(pathname, input_filename)
simulation_options = SimulationOptions(
save_to_disk=True, input_filename=input_filename, data_path=pathname, pml_size=10, data_cast="single"
save_to_disk=True, input_filename=input_filename, pml_size=10, data_cast="single"
)

# create a concave sensor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

# simulation settings
DATA_CAST = "single"
RUN_SIMULATION = False
RUN_SIMULATION = True

pml_size_points = Vector([20, 10, 10]) # [grid points]
grid_size_points = Vector([256, 128, 128]) - 2 * pml_size_points # [grid points]
Expand Down
6 changes: 4 additions & 2 deletions examples/us_bmode_phased_array/us_bmode_phased_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# simulation settings
DATA_CAST = "single"
RUN_SIMULATION = True
RNG_SEED = 123456


pml_size_points = Vector([15, 10, 10]) # [grid points]
Expand Down Expand Up @@ -83,18 +84,19 @@

not_transducer = NotATransducer(transducer, kgrid, **not_transducer)

rng = np.random.default_rng(RNG_SEED)

# Define a random distribution of scatterers for the medium
background_map_mean = 1
background_map_std = 0.008
background_map = background_map_mean + background_map_std * np.random.randn(kgrid.Nx, kgrid.Ny, kgrid.Nz)
background_map = background_map_mean + background_map_std * rng.standard_normal((kgrid.Nx, kgrid.Ny, kgrid.Nz))

sound_speed_map = c0 * background_map
density_map = rho0 * background_map


# Define a random distribution of scatterers for the highly scattering region
scattering_map = np.random.randn(kgrid.Nx, kgrid.Ny, kgrid.Nz)
scattering_map = rng.standard_normal((kgrid.Nx, kgrid.Ny, kgrid.Nz))
scattering_c0 = np.clip(c0 + 25 + 75 * scattering_map, 1400, 1600)
scattering_rho0 = scattering_c0 / 1.5

Expand Down
11 changes: 9 additions & 2 deletions kwave/kWaveSimulation_helper/save_to_disk_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@


def save_to_disk_func(
kgrid: kWaveGrid, medium: kWaveMedium, source, opt: SimulationOptions, auto_chunk: bool, values: dotdict, flags: dotdict
kgrid: kWaveGrid,
medium: kWaveMedium,
source,
opt: SimulationOptions,
auto_chunk: bool,
values: dotdict,
flags: dotdict,
input_filename: str | None = None,
):
# update command line status
logging.log(logging.INFO, f" precomputation completed in {scale_time(TicToc.toc())}")
Expand Down Expand Up @@ -63,7 +70,7 @@ def save_to_disk_func(
# =========================================================================

remove_z_dimension(float_variables, kgrid.dim)
save_file(opt.input_filename, integer_variables, float_variables, opt.hdf_compression_level, auto_chunk=auto_chunk)
save_file(input_filename or opt.input_filename, integer_variables, float_variables, opt.hdf_compression_level, auto_chunk=auto_chunk)

# update command line status
logging.log(logging.INFO, f" completed in {scale_time(TicToc.toc())}")
Expand Down
6 changes: 4 additions & 2 deletions kwave/kspaceFirstOrder2D.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from kwave.kWaveSimulation import kWaveSimulation
from kwave.kWaveSimulation_helper import retract_transducer_grid_size, save_to_disk_func
from kwave.options.simulation_execution_options import SimulationExecutionOptions
from kwave.options.simulation_options import SimulationOptions
from kwave.options.simulation_options import SimulationOptions, resolve_filenames_for_run
from kwave.utils.dotdictionary import dotdict
from kwave.utils.interp import interpolate2d
from kwave.utils.pml import get_pml
Expand Down Expand Up @@ -300,6 +300,7 @@ def kspaceFirstOrder2D(
if options.save_to_disk:
# store the pml size for resizing transducer object below
retract_size = [[options.pml_x_size, options.pml_y_size, options.pml_z_size]]
input_filename, output_filename = resolve_filenames_for_run(k_sim.options)

# run subscript to save files to disk
save_to_disk_func(
Expand Down Expand Up @@ -348,6 +349,7 @@ def kspaceFirstOrder2D(
"cuboid_corners": k_sim.cuboid_corners,
}
),
input_filename=input_filename,
)

# run subscript to resize the transducer object if the grid has been expanded
Expand All @@ -359,5 +361,5 @@ def kspaceFirstOrder2D(

executor = Executor(simulation_options=simulation_options, execution_options=execution_options)
executor_options = execution_options.as_list(sensor=k_sim.sensor)
sensor_data = executor.run_simulation(k_sim.options.input_filename, k_sim.options.output_filename, options=executor_options)
sensor_data = executor.run_simulation(input_filename, output_filename, options=executor_options)
return sensor_data
6 changes: 4 additions & 2 deletions kwave/kspaceFirstOrder3D.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from kwave.kWaveSimulation import kWaveSimulation
from kwave.kWaveSimulation_helper import retract_transducer_grid_size, save_to_disk_func
from kwave.options.simulation_execution_options import SimulationExecutionOptions
from kwave.options.simulation_options import SimulationOptions
from kwave.options.simulation_options import SimulationOptions, resolve_filenames_for_run
from kwave.utils.dotdictionary import dotdict
from kwave.utils.interp import interpolate3d
from kwave.utils.pml import get_pml
Expand Down Expand Up @@ -313,6 +313,7 @@ def kspaceFirstOrder3D(
if options.save_to_disk:
# store the pml size for resizing transducer object below
retract_size = [[options.pml_x_size, options.pml_y_size, options.pml_z_size]]
input_filename, output_filename = resolve_filenames_for_run(k_sim.options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find resolve_filenames_for_run function definition
ast-grep --pattern 'def resolve_filenames_for_run($_)'

Repository: waltsims/k-wave-python

Length of output: 3388


🏁 Script executed:

#!/bin/bash
# Read the function with its docstring
sed -n '439,480p' kwave/options/simulation_options.py

Repository: waltsims/k-wave-python

Length of output: 1828


🏁 Script executed:

#!/bin/bash
# Search for patterns where SimulationOptions might be reused across multiple runs
rg -n 'SimulationOptions' --type py | grep -E '(for|while|loop|multiple)' | head -20

Repository: waltsims/k-wave-python

Length of output: 2091


🏁 Script executed:

#!/bin/bash
# Look for examples that might reuse options objects
fd -e py -path '*/example*' -x grep -l 'resolve_filenames_for_run\|SimulationOptions' {} \;

Repository: waltsims/k-wave-python

Length of output: 234


🏁 Script executed:

#!/bin/bash
# Search examples directory for patterns of SimulationOptions reuse
find examples -name "*.py" -exec grep -l 'SimulationOptions' {} \;

Repository: waltsims/k-wave-python

Length of output: 1258


🏁 Script executed:

#!/bin/bash
# Check one of the example files to see if options are reused
head -150 examples/ivp_photoacoustic_waveforms/ivp_photoacoustic_waveforms.py

Repository: waltsims/k-wave-python

Length of output: 4295


🏁 Script executed:

#!/bin/bash
# Search for patterns where the same SimulationOptions variable is used in multiple simulation calls
rg -A 30 'simulation_options\s*=' examples/ | grep -B 5 'kspaceFirstOrder.*simulation_options' | head -60

Repository: waltsims/k-wave-python

Length of output: 5289


🏁 Script executed:

#!/bin/bash
# Check the checkpoint example which might reuse options
cat examples/checkpointing/checkpoint.py | head -200

Repository: waltsims/k-wave-python

Length of output: 5058


Undocumented side-effect: resolve_filenames_for_run mutates options.data_path.

At line 440–441 of kwave/options/simulation_options.py, the function silently sets options.data_path = _default_data_path() if it's None. This mutates the caller's SimulationOptions instance without any indication in the function signature or docstring. This could surprise code that reuses the same SimulationOptions across multiple runs. Either document this behavior clearly or refactor to return the resolved data_path alongside the filenames instead of mutating the input.


# run subscript to save files to disk
save_to_disk_func(
Expand Down Expand Up @@ -361,6 +362,7 @@ def kspaceFirstOrder3D(
"cuboid_corners": k_sim.cuboid_corners,
}
),
input_filename=input_filename,
)

# run subscript to resize the transducer object if the grid has been expanded
Expand All @@ -372,5 +374,5 @@ def kspaceFirstOrder3D(

executor = Executor(simulation_options=simulation_options, execution_options=execution_options)
executor_options = execution_options.as_list(sensor=k_sim.sensor)
sensor_data = executor.run_simulation(k_sim.options.input_filename, k_sim.options.output_filename, options=executor_options)
sensor_data = executor.run_simulation(input_filename, output_filename, options=executor_options)
return sensor_data
6 changes: 4 additions & 2 deletions kwave/kspaceFirstOrderAS.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from kwave.kWaveSimulation import kWaveSimulation
from kwave.kWaveSimulation_helper import retract_transducer_grid_size, save_to_disk_func
from kwave.options.simulation_execution_options import SimulationExecutionOptions
from kwave.options.simulation_options import SimulationOptions, SimulationType
from kwave.options.simulation_options import SimulationOptions, SimulationType, resolve_filenames_for_run
from kwave.utils.dotdictionary import dotdict
from kwave.utils.interp import interpolate2d
from kwave.utils.math import sinc
Expand Down Expand Up @@ -307,6 +307,7 @@ def kspaceFirstOrderAS(
if options.save_to_disk:
# store the pml size for resizing transducer object below
retract_size = [[options.pml_x_size, options.pml_y_size, options.pml_z_size]]
input_filename, output_filename = resolve_filenames_for_run(k_sim.options)

# run subscript to save files to disk
save_to_disk_func(
Expand Down Expand Up @@ -355,6 +356,7 @@ def kspaceFirstOrderAS(
"cuboid_corners": k_sim.cuboid_corners,
}
),
input_filename=input_filename,
)

# run subscript to resize the transducer object if the grid has been expanded
Expand All @@ -366,5 +368,5 @@ def kspaceFirstOrderAS(

executor = Executor(simulation_options=simulation_options, execution_options=execution_options)
executor_options = execution_options.as_list(sensor=k_sim.sensor)
sensor_data = executor.run_simulation(k_sim.options.input_filename, k_sim.options.output_filename, options=executor_options)
sensor_data = executor.run_simulation(input_filename, output_filename, options=executor_options)
return sensor_data
Loading
Loading