diff --git a/.github/workflows/generate-notebooks.yml b/.github/workflows/generate-notebooks.yml new file mode 100644 index 000000000..9f3ddac20 --- /dev/null +++ b/.github/workflows/generate-notebooks.yml @@ -0,0 +1,42 @@ +name: Generate Notebooks + +on: + push: + branches: [master] + paths: ['examples/*.py'] + +jobs: + generate: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install jupytext + run: pip install jupytext + + - name: Generate notebooks + run: | + mkdir -p notebooks + for py in examples/*.py; do + [ -f "$py" ] || continue + basename=$(basename "$py") + jupytext --to notebook "$py" --output "notebooks/${basename%.py}.ipynb" + done + + - name: Commit notebooks + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add notebooks/ + if git diff --cached --quiet; then + echo "No notebook changes" + else + git commit -m "Auto-generate notebooks from examples [skip ci]" + git push origin HEAD:master + fi diff --git a/.github/workflows/run-examples.yml b/.github/workflows/run-examples.yml index ec470a284..029e15fa0 100644 --- a/.github/workflows/run-examples.yml +++ b/.github/workflows/run-examples.yml @@ -14,8 +14,10 @@ jobs: - uses: actions/checkout@v4 - id: find-examples run: | - # Find all Python files in examples subdirectories - EXAMPLES=$(find examples -name "*.py" -not -path "*/\.*" | jq -R -s -c 'split("\n")[:-1]') + # Find all example scripts (exclude utilities only) + EXAMPLES=$(find examples -maxdepth 2 -name "*.py" \ + -not -name "example_utils.py" \ + | sort | jq -R -s -c 'split("\n")[:-1]') echo "examples=$EXAMPLES" >> "$GITHUB_OUTPUT" run-examples: @@ -50,7 +52,8 @@ jobs: - name: Run example env: - KWAVE_FORCE_CPU: 1 + KWAVE_DEVICE: cpu + MPLBACKEND: Agg run: | echo "Running example: ${{ matrix.example }}" - python "${{ matrix.example }}" + python "${{ matrix.example }}" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..92ebad62f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,89 @@ +# CLAUDE.md — k-wave-python + +## What this project is + +k-Wave-python is a Python port of [k-Wave](http://www.k-wave.org/), a MATLAB toolbox for time-domain acoustic and ultrasound simulations using the k-space pseudospectral method. It supports 1D, 2D, and 3D simulations with optional GPU acceleration via CuPy. + +## Key commands + +```bash +uv run pytest tests/ # Run all tests +uv run pytest tests/test_native_solver.py # Run solver tests +uv run python run_examples.py # Run all examples (python backend) +uv run python examples/ivp_1D_simulation.py # Run a single example +``` + +## Architecture + +- **Entry point:** `kwave/kspaceFirstOrder.py` — `kspaceFirstOrder()` is the unified API +- **Python solver:** `kwave/solvers/kspace_solver.py` — `Simulation` class (setup/step/run) +- **C++ backend:** `kwave/solvers/cpp_simulation.py` — HDF5 serialization + binary execution +- **Validation:** `kwave/solvers/validation.py` +- **Legacy bridge:** `kwave/solvers/native.py` — minimal wrapper for deprecated `kspaceFirstOrder2D/3D` +- **MATLAB interop:** `simulate_from_dicts()` in `kspace_solver.py`, shim in k-wave-cupy repo + +## Critical conventions + +### Memory layout: Fortran vs C order + +k-Wave is a MATLAB project. MATLAB uses **column-major (Fortran) order**. NumPy defaults to **row-major (C) order**. + +**Current state:** The solver uses `order="F"` extensively for MATLAB compatibility. This is being migrated: +- **Internal solver logic** (FFT, PML, stepping) is order-agnostic +- **Boundary layers** (HDF5 serialization, MATLAB interop) must use F-order +- **Goal for v1.0:** C-order internally, F-order only at boundaries + +When modifying array reshaping/flattening code, always check whether `order="F"` is needed for correctness or is legacy MATLAB convention. + +### Indexing: 0-based vs 1-based + +MATLAB uses 1-based indexing. Two places where this matters: + +1. **`sensor.record_start_index`** — uses 1-based (MATLAB convention). Converted to 0-based internally in `_setup_sensor_mask`. +2. **C++ HDF5 format** — sensor/source indices are 1-based. The `+1` in `cpp_simulation.py` is intentional. +3. **`matlab_find()`** — returns 1-based indices. Used by `kWaveArray`. + +### k-wave-cupy interop + +The k-wave-cupy repo contains the MATLAB wrapper (`kspaceFirstOrderPy.m`) that calls Python via `py.kWavePy.simulate_from_dicts()`. The `kWavePy.py` shim in that repo re-exports from k-wave-python. + +- `simulate_from_dicts(device=...)` — NOT `backend=`. The parameter was renamed. +- MATLAB passes dicts with F-order arrays and 1-based indices +- The shim handles `pml_inside=False` expansion externally + +### Backend vs Device naming + +- `backend` = `"python"` or `"cpp"` (which engine runs the simulation) +- `device` = `"cpu"` or `"gpu"` (hardware target) + +These are separate concerns. `kspaceFirstOrder(backend="python", device="gpu")` runs the Python solver on GPU via CuPy. + +## Environment variables + +- `KWAVE_BACKEND` — Override backend for all `kspaceFirstOrder()` calls (used by CI) +- `KWAVE_DEVICE` — Override device for all `kspaceFirstOrder()` calls (used by CI) + +## Examples + +Examples are `.py` files with `# %%` cell markers (percent format). They work as: +- Plain scripts: `python examples/foo.py` +- Interactive notebooks: Open in VS Code/JupyterLab +- Jupyter notebooks: Generated via `jupytext --to notebook` + +Source of truth is always the `.py` file. Never edit `.ipynb` directly. + +## Testing + +```bash +uv run pytest tests/test_native_solver.py # Solver + physics tests +uv run pytest tests/test_validation.py # Input validation +uv run pytest tests/test_kspaceFirstOrder.py # Entry point tests +uv run pytest tests/test_compat.py # Legacy options migration +``` + +## Style + +- Simple > less code > fast +- No backward compatibility aliases — clean breaks +- `quiet=True` to silence, `debug=True` for verbose (not `verbose=True`) +- Use `uv run` for all tools (pytest, python, etc.) diff --git a/examples/README.md b/examples/README.md index de463edba..4d94a755a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,57 +1,52 @@ -# Examples - -Many examples from k-wave-python are Python mirrors of the [large collection of great examples](http://www.k-wave.org/documentation/k-wave_examples.php) from the original k-wave project. -All examples are included as python files or notebooks in a subdirectory of the example directory. When running the examples in Google Colab, remember to select a GPU runtime for GPU simulations. -Every example has a short readme.md file which briefly describes the purpose of the example. - -## List of Examples - -- [Array as a sensor](at_array_as_sensor/) ([original example](http://www.k-wave.org/documentation/example_at_array_as_sensor.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_array_as_sensor/at_array_as_sensor.ipynb)) -- [Array as a source](at_array_as_source/) ([original example](http://www.k-wave.org/documentation/example_at_array_as_source.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_array_as_source/at_array_as_source.ipynb)) -- [Linear array transducer](at_linear_array_transducer/) - ([original example](http://www.k-wave.org/documentation/example_at_linear_array_transducer.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_linear_array_transducer/at_linear_array_transducer.ipynb)) -- [Photoacoustic Waveforms](ivp_photoacoustic_waveforms/) ([original example](http://www.k-wave.org/documentation/example_ivp_photoacoustic_waveforms.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/ivp_photoacoustic_waveforms/ivp_photoacoustic_waveforms.ipynb)) -- [Controlling the PML](na_controlling_the_pml/) - ([original example](http://www.k-wave.org/documentation/example_na_controlling_the_pml.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/na_controlling_the_pml/na_controlling_the_pml.ipynb)) -- [Defining An Ultrasound Transducer Example](us_defining_transducer) ([original example](http://www.k-wave.org/documentation/example_us_defining_transducer.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/us_defining_transducer/us_defining_transducer.ipynb)) -- [Simulating Ultrasound Beam Patterns](us_beam_patterns/) ([original example](http://www.k-wave.org/documentation/example_us_beam_patterns), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/us_beam_patterns/us_beam_patterns.ipynb)) -- [Linear transducer B-mode](us_bmode_linear_transducer/) ([original example](http://www.k-wave.org/documentation/example_us_bmode_linear_transducer.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/us_bmode_linear_transducer/us_bmode_linear_transducer.ipynb)) -- [Phased array B-mode](us_bmode_phased_array/) - ([original example](http://www.k-wave.org/documentation/example_us_bmode_phased_array.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/us_bmode_phased_array/us_bmode_phased_array.ipynb)) -- [Circular piston transducer](at_circular_piston_3D/) ([original example](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading3), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_circular_piston_3D/at_circular_piston_3D.ipynb)) - -- [Circular piston transducer (axisymmetric)](at_circular_piston_AS/) ([original example](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading4), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_circular_piston_AS/at_circular_piston_AS.ipynb)) - -- [Focused bowl transducer](at_focused_bowl_3D/) ([original example](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading5), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_focused_bowl_3D/at_focused_bowl_3D.ipynb)) - -- [Focused bowl transducer (axisymmetric)](at_focused_bowl_AS/) ([original example](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading6), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_focused_bowl_AS/at_focused_bowl_AS.ipynb)) - -- [Focused annular array](at_focused_annular_array_3D/) ([original example](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading7), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.ipynb)) -- [2D FFT Reconstruction For A Line Sensor](pr_2D_FFT_line_sensor/) ([original example](http://www.k-wave.org/documentation/example_pr_2D_fft_line_sensor.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/pr_2D_FFT_line_sensor/pr_2D_FFT_line_sensor.ipynb)) -- [3D FFT Reconstruction For A Planar Sensor](pr_3D_FFT_planar_sensor/) ([original example](http://www.k-wave.org/documentation/example_pr_3D_fft_planar_sensor.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/pr_3D_FFT_planar_sensor/pr_3D_FFT_planar_sensor.ipynb)) -- [2D Time Reversal Reconstruction For A Line Sensor](pr_2D_TR_line_sensor/) ([original example](http://www.k-wave.org/documentation/example_pr_2D_tr_line_sensor.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/pr_2D_TR_line_sensor/pr_2D_TR_line_sensor.ipynb)) -- [3D Time Reversal Reconstruction For A Planar Sensor](pr_3D_TR_planar_sensor/) ([original example](http://www.k-wave.org/documentation/example_pr_3D_tr_planar_sensor.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/pr_3D_TR_planar_sensor/pr_3D_TR_planar_sensor.ipynb)) - - -- [Focussed Detector In 2D Example](sd_focussed_detector_2D/) ([original example](http://www.k-wave.org/documentation/example_sd_focussed_detector_2D.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/sd_focussed_detector_2D/sd_focussed_detector_2D.ipynb)) - -- [Focussed Detector In 3D Example](sd_focussed_detector_3D/) ([original example](http://www.k-wave.org/documentation/example_sd_focussed_detector_3D.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/sd_focussed_detector_3D/sd_focussed_detector_3D.ipynb)) - -- [Modelling Sensor Directivity In 2D Example](sd_directivity_modelling_2D/) ([original example](http://www.k-wave.org/documentation/example_sd_directivity_modelling_2D.php), [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/sd_directivity_modelling_2D/sd_directivity_modelling_2D.ipynb)) - -## Contributing new examples - -When adding a new example notebook, follow these steps: - -1. Search the [list of examples](https://docs.google.com/spreadsheets/d/1-x13iIez84AEyjjHMOe2GoC8FSyzUFHoy9R7VKttTlo/edit?usp=sharing) to find an open example. -1. Claim your example by opening an "Example" issue on GitHub. -1. Fork and clone the repository and create a branch for your new example. -1. Create an example sub-directory using the name from the hyperlink of the original k-wave example if it exists (e.g. for http://www.k-wave.org/documentation/example_ivp_loading_external_image.php name the directory "ivp_loading_external_image). -1. Add your example notebook to your example directory. -1. Create a Python script that mirrors your example notebook in the same directory. Using `nbconvert` is an easy way to convert to a Python script using ` jupyter nbconvert --to python --RegexRemovePreprocessor.patterns="^%" ` -1. Add a README.md file to your example directory briefly describing the concept or principle the example is meant to display and linking to the origonal k-wave example page if it exists. -1. Include a link in the `README.md` in the examples directory to a colab notebook for your example. -1. Add a your example to the list on this `README.md` and add a colab badge [using html](https://openincolab.com/) OR copy the pure markdown version above. -1. Open a pull request that [closes the open issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) from your forked example branch and name pull request "[Example] \". - -Thanks for contributing to k-wave-python! +# k-Wave Examples + +Each `.py` file is both a runnable script and an interactive notebook. + +## Running examples + +**As a script:** +```bash +python examples/ivp_1D_simulation.py +``` + +**Interactively (VS Code / JupyterLab / PyCharm):** +Open any `.py` file — the `# %%` markers define cells you can run one at a time. + +**On Google Colab:** +Notebooks are auto-generated in the [`notebooks/`](../notebooks/) directory. Open any example with: + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/master/notebooks/ivp_1D_simulation.ipynb) + +Replace the filename in the URL for other examples. + +## Example categories + +| Prefix | Topic | +|--------|-------| +| `ivp_` | Initial value problems (photoacoustics) | +| `pr_` | Photoacoustic reconstruction (FFT, time reversal) | +| `sd_` | Sensor directivity and focused detectors | +| `at_` | Acoustic transducers (pistons, bowls, arrays) | +| `us_` | Ultrasound imaging (B-mode, beam patterns) | +| `na_` | Numerical analysis (PML, performance) | + +## Writing a new example + +1. Create `examples/your_example/your_example.py` +2. Start with a markdown header and imports: + ```python + # %% [markdown] + # # Your Example Title + # One-line description of what this demonstrates. + + # %% + import numpy as np + from kwave.kgrid import kWaveGrid + from kwave.kmedium import kWaveMedium + from kwave.ksource import kSource + from kwave.ksensor import kSensor + from kwave.kspaceFirstOrder import kspaceFirstOrder + ``` +3. Add `# %%` before each logical section (grid, source, simulation, visualization) +4. Use `kspaceFirstOrder()` — not the legacy `kspaceFirstOrder2D/3D` +5. Notebooks are auto-generated from `.py` files on merge to master — don't create `.ipynb` by hand diff --git a/examples/at_array_as_sensor/at_array_as_sensor.py b/examples/at_array_as_sensor.py similarity index 80% rename from examples/at_array_as_sensor/at_array_as_sensor.py rename to examples/at_array_as_sensor.py index a93458cae..8a2f03da0 100644 --- a/examples/at_array_as_sensor/at_array_as_sensor.py +++ b/examples/at_array_as_sensor.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # Array As Sensor +# Using a kWaveArray of arc elements as a sensor for photoacoustic imaging. + +# %% import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np @@ -7,9 +12,7 @@ from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor from kwave.ksource import kSource -from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.utils.colormap import get_color_map from kwave.utils.conversion import cart2grid from kwave.utils.kwave_array import kWaveArray @@ -18,6 +21,7 @@ def main(): + # %% Define array and grid # create empty array karray = kWaveArray() @@ -46,6 +50,7 @@ def main(): # time array kgrid.makeTime(medium.sound_speed) + # %% Source and sensor source = kSource() x_offset = 20 # [pixels] # make a small disc in the top left of the domain @@ -54,30 +59,23 @@ def main(): logical_p0 = source.p0.astype(bool) sensor = kSensor() sensor.mask = element_pos - simulation_options = SimulationOptions( - save_to_disk=True, - data_cast="single", - ) - execution_options = SimulationExecutionOptions(is_gpu_simulation=True) - output = kspaceFirstOrder2D(kgrid, source, sensor, medium, simulation_options, execution_options) + # %% Run simulations + # NOTE: data_cast="single" not supported in new API + output = kspaceFirstOrder(kgrid, medium, source, sensor, backend="cpp", device="gpu") # Reorder the sensor data returned by k-Wave to match the order of the elements in the array _, _, reorder_index = cart2grid(kgrid, element_pos) - sensor_data_point = reorder_binary_sensor_data(output["p"].T, reorder_index=reorder_index) + sensor_data_point = reorder_binary_sensor_data(output["p"], reorder_index=reorder_index) # assign binary mask from karray to the source mask sensor.mask = karray.get_array_binary_mask(kgrid) - output = kspaceFirstOrder2D(kgrid, source, sensor, medium, simulation_options, execution_options) - sensor_data = output["p"].T - combined_sensor_data = karray.combine_sensor_data(kgrid, sensor_data) - - # ========================================================================= - # VISUALIZATION - # ========================================================================= + output = kspaceFirstOrder(kgrid, medium, source, sensor, backend="cpp", device="gpu") + combined_sensor_data = karray.combine_sensor_data(kgrid, output["p"]) - # create pml mask (reuse default size of 20 grid points from simulation_options) - pml_size = simulation_options.pml_x_size # 20 [grid_points] + # %% Visualization + # create pml mask (default size of 20 grid points) + pml_size = 20 # [grid_points] pml_mask = np.zeros((N.x, N.y), dtype=bool) pml_mask[:pml_size, :] = 1 pml_mask[:, :pml_size] = 1 diff --git a/examples/at_array_as_sensor/README.md b/examples/at_array_as_sensor/README.md deleted file mode 100644 index 928ec3daa..000000000 --- a/examples/at_array_as_sensor/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Defining A Sensor Using An Array Transducer Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_array_as_sensor/at_array_as_sensor.ipynb) - -This example provides a demonstration of using the kWaveArray class to define an array transducer with 20 arc-shaped elements which is then used as a receiver array. It builds on the Defining A Source Using An Array Transducer Example. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_at_array_as_sensor.php) \ No newline at end of file diff --git a/examples/at_array_as_sensor/at_array_as_sensor.ipynb b/examples/at_array_as_sensor/at_array_as_sensor.ipynb deleted file mode 100644 index 87a4f520f..000000000 --- a/examples/at_array_as_sensor/at_array_as_sensor.ipynb +++ /dev/null @@ -1,299 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib as mpl\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.colormap import get_color_map\n", - "from kwave.utils.conversion import cart2grid\n", - "from kwave.utils.kwave_array import kWaveArray\n", - "from kwave.utils.mapgen import make_cart_circle, make_disc\n", - "from kwave.utils.signals import reorder_binary_sensor_data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define arc properties\n", - "radius = 100e-3 # [m]\n", - "diameter = 8e-3 # [m]\n", - "ring_radius = 50e-3 # [m]\n", - "num_elements = 20\n", - "\n", - "# orient all elements towards the center of the grid\n", - "focus_pos = Vector([0, 0]) # [m]\n", - "\n", - "element_pos = make_cart_circle(ring_radius, num_elements, focus_pos)\n", - "\n", - "# create empty array\n", - "karray = kWaveArray()\n", - "\n", - "for idx in range(num_elements):\n", - " karray.add_arc_element(element_pos[:, idx], radius, diameter, focus_pos)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "# medium properties\n", - "medium = kWaveMedium(sound_speed=1500)\n", - "\n", - "# grid properties\n", - "N = Vector([256, 256])\n", - "d = Vector([0.5e-3, 0.5e-3])\n", - "kgrid = kWaveGrid(N, d)\n", - "# time array\n", - "kgrid.makeTime(medium.sound_speed)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "source = kSource()\n", - "x_offset = 20 # [pixels]\n", - "# make a small disc in the top left of the domain\n", - "source.p0 = make_disc(N, Vector([N.x / 4 + x_offset, N.y / 4]), 4)\n", - "source.p0[99:119, 59:199] = 1\n", - "logical_p0 = source.p0.astype(bool)\n", - "sensor = kSensor()\n", - "sensor.mask = element_pos" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "simulation_options = SimulationOptions(\n", - " save_to_disk=True,\n", - " data_cast='single',\n", - ")\n", - "\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True)\n", - "output = kspaceFirstOrder2D(kgrid, source, sensor, medium, simulation_options, execution_options)\n", - "\n", - "# Here we reorder the sensor data returned by k-Wave to match the order of the elements in the array\n", - "_, _, reorder_index = cart2grid(kgrid, element_pos)\n", - "sensor_data_point = reorder_binary_sensor_data(output['p'].T, reorder_index=reorder_index)\n", - "\n", - "# assign binary mask from karray to the source mask\n", - "sensor.mask = karray.get_array_binary_mask(kgrid)\n", - "\n", - "output = kspaceFirstOrder2D(kgrid, source, sensor, medium, simulation_options, execution_options)\n", - "sensor_data = output['p'].T\n", - "combined_sensor_data = karray.combine_sensor_data(kgrid, sensor_data)\n", - "\n", - "\n", - "# create pml mask (reuse default size of 20 grid points from simulation_options)\n", - "pml_size = simulation_options.pml_x_size # 20 [grid_points]\n", - "pml_mask = np.zeros((N.x, N.y), dtype=bool)\n", - "pml_mask[:pml_size, :] = 1\n", - "pml_mask[:, :pml_size] = 1\n", - "pml_mask[-pml_size:, :] = 1\n", - "pml_mask[:, -pml_size:] = 1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Visualization" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "# create pml mask (reuse default size of 20 grid points from simulation_options)\n", - "pml_size = simulation_options.pml_x_size # 20 [grid_points]\n", - "pml_mask = np.zeros((N.x, N.y), dtype=bool)\n", - "pml_mask[:pml_size, :] = 1\n", - "pml_mask[:, :pml_size] = 1\n", - "pml_mask[-pml_size:, :] = 1\n", - "pml_mask[:, -pml_size:] = 1\n", - "\n", - "# Plot source, sensor, and pml masks\n", - "\n", - "# Assign unique values to each mask\n", - "sensor_val = sensor.mask * 1\n", - "logical_p0_val = logical_p0 * 2\n", - "pml_mask_val = pml_mask * 3\n", - "\n", - "# Combine masks\n", - "combined_mask = sensor_val + logical_p0_val + pml_mask_val\n", - "combined_mask = np.flipud(combined_mask)\n", - "\n", - "# Define custom colormap\n", - "colors = [\n", - " (1, 1, 1), # White (Background)\n", - " (233/255, 131/255, 0/255), # Orange (Sensor)\n", - " (254/255, 221/255, 92/255), # Yellow (Sources)\n", - " (0.8, 0.8, 0.8), # Light Grey (PML Mask)\n", - "]\n", - "cmap = plt.matplotlib.colors.ListedColormap(colors)\n", - "\n", - "fig, ax = plt.subplots()\n", - "c = ax.pcolormesh(combined_mask, cmap=cmap, shading='auto')\n", - "for element in karray.elements:\n", - " ax.plot(*element.position // kgrid.spacing + kgrid.N // 2, 'kx') \n", - "plt.axis('image')\n", - "\n", - "# Define labels for the colorbar\n", - "labels = {\n", - " 0: 'None',\n", - " 1: 'Sensor',\n", - " 2: 'Initial pressure p0',\n", - " 3: 'PML Mask',\n", - "}\n", - "\n", - "bounds = np.linspace(0, len(labels), len(labels)+1)\n", - "norm = mpl.colors.BoundaryNorm(bounds, cmap.N)\n", - "\n", - "ax2 = fig.add_axes([0.95, 0.1, 0.03, 0.8])\n", - "cb = mpl.colorbar.ColorbarBase(ax2, cmap=cmap, norm=norm,\n", - " spacing='proportional', ticks=bounds, boundaries=bounds, format='%1i')\n", - "\n", - "# Update the title and label as before\n", - "ax.set_title('Simulation Layout')\n", - "ax2.set_ylabel('Simulation Components [-]', size=12)\n", - "\n", - "# Calculate the middle points for each segment of the colorbar\n", - "mid_points = [(bounds[i] + bounds[i+1])/2 for i in range(len(bounds)-1)]\n", - "\n", - "# Set the new tick positions and labels\n", - "ax2.set_yticks(mid_points)\n", - "ax2.set_yticklabels(list(labels.values()))\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig, [ax1, ax2] = plt.subplots(ncols=1, nrows=2)\n", - "im1 = ax1.imshow(sensor_data_point, aspect=\"auto\", cmap=get_color_map(), interpolation=\"none\")\n", - "ax1.set_xlabel(r\"Time [$\\mu$s]\")\n", - "ax1.set_ylabel(\"Detector Number\")\n", - "ax1.set_title(\"Cartesian point detectors\")\n", - "fig.colorbar(im1, ax=ax1)\n", - "\n", - "im2 = ax2.imshow(combined_sensor_data, aspect=\"auto\", cmap=get_color_map(), interpolation=\"none\")\n", - "ax2.set_xlabel(r\"Time [$\\mu$s]\")\n", - "ax2.set_ylabel(\"Detector Number\")\n", - "ax2.set_title(\"Arc detectors\")\n", - "fig.colorbar(im2, ax=ax2)\n", - "\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig.subplots_adjust(hspace=0.5)\n", - "\n", - "# Plot a trace from the recorded sensor data\n", - "fig = plt.figure()\n", - "plt.plot(kgrid.t_array.squeeze() * 1e6, sensor_data_point[0, :], label='Cartesian point detectors')\n", - "plt.plot(kgrid.t_array.squeeze() * 1e6, combined_sensor_data[0, :], label='Arc detectors')\n", - "plt.xlabel(r'Time [$\\mu$s]')\n", - "plt.ylabel('Pressure [pa]')\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig.subplots_adjust(hspace=0.5)\n", - "\n", - "# Plot a trace from the recorded sensor data\n", - "fig = plt.figure()\n", - "plt.plot(kgrid.t_array.squeeze() * 1e6, combined_sensor_data[0, :] - sensor_data_point[0, :])\n", - "plt.xlabel(r'Time [$\\mu$s]')\n", - "plt.ylabel('Pressure [pa]')\n", - "plt.title('Difference between Cartesian and Arc detectors')\n", - "plt.legend()\n", - "plt.show()" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/at_array_as_source/at_array_as_source.py b/examples/at_array_as_source.py similarity index 77% rename from examples/at_array_as_source/at_array_as_source.py rename to examples/at_array_as_source.py index 9ebbb7318..83a1eb5c6 100644 --- a/examples/at_array_as_source/at_array_as_source.py +++ b/examples/at_array_as_source.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # Array As Source +# Using a kWaveArray of arc elements as a pressure source with multiple frequencies. + +# %% import matplotlib.pyplot as plt import numpy as np from matplotlib import colors @@ -8,19 +13,14 @@ from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor from kwave.ksource import kSource -from kwave.kspaceFirstOrder2D import kspace_first_order_2d_gpu -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.utils.colormap import get_color_map from kwave.utils.kwave_array import kWaveArray from kwave.utils.signals import tone_burst def main(): - # ========================================================================= - # DEFINE KWAVEARRAY - # ========================================================================= - + # %% Define kWaveArray # create empty array karray = kWaveArray() @@ -45,10 +45,7 @@ def main(): # elements together) karray.set_array_position([10e-3, 0], 10) - # ========================================================================= - # DEFINE GRID PROPERTIES - # ========================================================================= - + # %% Grid and medium # grid properties Nx = 256 dx = 0.5e-3 @@ -62,10 +59,7 @@ def main(): # time array kgrid.makeTime(medium.sound_speed) - # ========================================================================= - # SIMULATION - # ========================================================================= - + # %% Source and simulation # assign binary mask from karray to the source mask source_p_mask = karray.get_array_binary_mask(kgrid) @@ -87,11 +81,7 @@ def main(): # source signal for each grid point that forms part of the source) source_p = karray.get_distributed_source_signal(kgrid, source_signal) - simulation_options = SimulationOptions( - save_to_disk=True, - data_cast="single", - ) - execution_options = SimulationExecutionOptions(is_gpu_simulation=True) + # NOTE: data_cast="single" not supported in new API # run k-Wave simulation (no sensor is used for this example) # TODO: I would say proper behaviour would be to return the entire pressure field if sensor is None sensor = kSensor() @@ -101,14 +91,12 @@ def main(): source.p_mask = source_p_mask source.p = source_p - p = kspace_first_order_2d_gpu(kgrid, source, sensor, medium, simulation_options, execution_options) + p = kspaceFirstOrder(kgrid, medium, source, sensor, backend="cpp", device="gpu") p_field = np.reshape(p["p"], (kgrid.Nt, Nx, Ny)) p_field = np.transpose(p_field, (0, 2, 1)) - # ========================================================================= - # VISUALIZATION - # ========================================================================= + # %% Visualization # Normalize frames based on the maximum value over all frames max_value = np.max(p_field) normalized_frames = p_field / max_value diff --git a/examples/at_array_as_source/README.md b/examples/at_array_as_source/README.md deleted file mode 100644 index 2c75d5a00..000000000 --- a/examples/at_array_as_source/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Defining A Source Using An Array Transducer Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_array_as_source/at_array_as_source.ipynb) - -This example provides a demonstration of using the kWaveArray class to define an array transducer with three arc-shaped elements without staircasing errors. - -For a more detailed discussion of this example and the underlying techniques, see [E. S. Wise, B. T. Cox, J. Jaros, & B. E. Treeby (2019). Representing arbitrary acoustic source and sensor distributions in Fourier collocation methods. The Journal of the Acoustical Society of America, 146(1), 278-288.](https://pubs.aip.org/asa/jasa/article/146/1/278/993785/Representing-arbitrary-acoustic-source-and-sensor). - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_at_array_as_source.php). diff --git a/examples/at_array_as_source/at_array_as_source.ipynb b/examples/at_array_as_source/at_array_as_source.ipynb deleted file mode 100644 index ba7453d8f..000000000 --- a/examples/at_array_as_source/at_array_as_source.ipynb +++ /dev/null @@ -1,262 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from matplotlib import colors\n", - "from matplotlib.animation import FuncAnimation\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder2D import kspace_first_order_2d_gpu\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.colormap import get_color_map\n", - "from kwave.utils.kwave_array import kWaveArray\n", - "from kwave.utils.signals import tone_burst\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Define kWaveArray" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create empty array\n", - "karray = kWaveArray()\n", - "\n", - "# define arc properties\n", - "radius = 50e-3 # [m]\n", - "diameter = 30e-3 # [m]\n", - "focus_pos = [-20e-3, 0] # [m]\n", - "\n", - "# add arc-shaped element\n", - "elem_pos = [10e-3, -40e-3] # [m]\n", - "karray.add_arc_element(elem_pos, radius, diameter, focus_pos)\n", - "\n", - "# add arc-shaped element\n", - "elem_pos = [20e-3, 0] # [m]\n", - "karray.add_arc_element(elem_pos, radius, diameter, focus_pos)\n", - "\n", - "# add arc-shaped element\n", - "elem_pos = [10e-3, 40e-3] # [m]\n", - "karray.add_arc_element(elem_pos, radius, diameter, focus_pos)\n", - "\n", - "# move the array down 10 mm, and rotate by 10 degrees (this moves all the\n", - "# elements together)\n", - "karray.set_array_position([10e-3, 0], 10)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Define grid properties" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "# grid properties\n", - "Nx = 256\n", - "dx = 0.5e-3\n", - "Ny = 256\n", - "dy = 0.5e-3\n", - "kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))\n", - "\n", - "# medium properties\n", - "medium = kWaveMedium(sound_speed=1500)\n", - "\n", - "# time array\n", - "kgrid.makeTime(medium.sound_speed)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# assign binary mask from karray to the source mask\n", - "source_p_mask = karray.get_array_binary_mask(kgrid)\n", - "\n", - "# set source signals, one for each physical array element\n", - "f1 = 100e3\n", - "f2 = 200e3\n", - "f3 = 500e3\n", - "sig1 = tone_burst(1 / kgrid.dt, f1, 3).squeeze()\n", - "sig2 = tone_burst(1 / kgrid.dt, f2, 5).squeeze()\n", - "sig3 = tone_burst(1 / kgrid.dt, f3, 5).squeeze()\n", - "\n", - "# combine source signals into one array\n", - "source_signal = np.zeros((3, max(len(sig1), len(sig2))))\n", - "source_signal[0, :len(sig1)] = sig1\n", - "source_signal[1, :len(sig2)] = sig2\n", - "source_signal[2, :len(sig3)] = sig3\n", - "\n", - "# get distributed source signals (this automatically returns a weighted\n", - "# source signal for each grid point that forms part of the source)\n", - "source_p = karray.get_distributed_source_signal(kgrid, source_signal)\n", - "\n", - "simulation_options = SimulationOptions(\n", - " save_to_disk=True,\n", - " data_cast='single',\n", - ")\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True)\n", - "# run k-Wave simulation (no sensor is used for this example)\n", - "# TODO: I would say proper behaviour would be to return the entire pressure field if sensor is None\n", - "sensor = kSensor()\n", - "sensor.mask = np.ones((Nx, Ny), dtype=bool)\n", - "\n", - "source = kSource()\n", - "source.p_mask = source_p_mask\n", - "source.p = source_p\n", - "\n", - "p = kspace_first_order_2d_gpu(kgrid, source, sensor, medium, simulation_options, execution_options)\n", - "\n", - "p_field = np.reshape(p['p'], (kgrid.Nt, Nx, Ny))\n", - "p_field = np.transpose(p_field, (0, 2, 1))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Visualization" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib import rc\n", - "\n", - "rc('animation', html='jshtml', embed_limit=10**3)\n", - "%matplotlib notebook\n", - "\n", - "# Normalize frames based on the maximum value over all frames\n", - "max_value = np.max(p_field)\n", - "normalized_frames = p_field / max_value\n", - "\n", - "cmap = get_color_map()\n", - "\n", - "# Create a figure and axis\n", - "fig, ax = plt.subplots()\n", - "\n", - "# Create an empty image with the first normalized frame\n", - "image = ax.imshow(normalized_frames[0], cmap=cmap, norm=colors.Normalize(vmin=0, vmax=1))\n", - "\n", - "# Function to update the image for each frame\n", - "def update(frame):\n", - " image.set_data(normalized_frames[frame])\n", - " ax.set_title(f'Frame {frame + 1}/{kgrid.Nt}')\n", - " return [image]\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create the animation\n", - "ani = FuncAnimation(fig, update, frames=kgrid.Nt, interval=5, blit=False, repeat=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ani # This takes time" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create pml mask (default size in 2D is 20 grid points)\n", - "%matplotlib inline\n", - "pml_size = 20\n", - "pml_mask = np.zeros((Nx, Ny), dtype=bool)\n", - "pml_mask[:pml_size, :] = 1\n", - "pml_mask[:, :pml_size] = 1\n", - "pml_mask[-pml_size:, :] = 1\n", - "pml_mask[:, -pml_size:] = 1\n", - "\n", - "# plot source and pml masks\n", - "plt.figure()\n", - "plt.imshow(np.logical_not(np.squeeze(source.p_mask | pml_mask)), aspect='auto', cmap='gray')\n", - "plt.xlabel('x-position [m]')\n", - "plt.ylabel('y-position [m]')\n", - "plt.title('Source and PML Masks')" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/at_circular_piston_3D/at_circular_piston_3D.py b/examples/at_circular_piston_3D.py similarity index 77% rename from examples/at_circular_piston_3D/at_circular_piston_3D.py rename to examples/at_circular_piston_3D.py index 4d7bc1398..6581f0368 100644 --- a/examples/at_circular_piston_3D/at_circular_piston_3D.py +++ b/examples/at_circular_piston_3D.py @@ -1,207 +1,196 @@ -from copy import deepcopy - -import matplotlib.pyplot as plt -import numpy as np - -from kwave.data import Vector -from kwave.kgrid import kWaveGrid -from kwave.kmedium import kWaveMedium -from kwave.ksensor import kSensor -from kwave.ksource import kSource -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions -from kwave.utils.filters import extract_amp_phase -from kwave.utils.kwave_array import kWaveArray -from kwave.utils.math import round_even -from kwave.utils.signals import create_cw_signals - -# material values -c0: float = 1500.0 # sound speed [m/s] -rho0: float = 1000.0 # density [kg/m^3] - -# source parameters -source_f0: float = 1e6 # source frequency [Hz] -source_diam: float = 10e-3 # piston diameter [m] -source_amp: float = 1e6 # source pressure [Pa] -source_phase: float = 0.0 # source pressure [Pa] - -# grid parameters -axial_size: float = 32e-3 # total grid size in the axial dimension [m] -lateral_size: float = 23e-3 # total grid size in the lateral dimension [m] - -# computational parameters -ppw = 3 # number of points per wavelength -t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state) -record_periods: int = 1 # number of periods to record -cfl: float = 0.5 # CFL number -bli_tolerance: float = 0.03 # tolerance for truncation of the off-grid source points -upsampling_rate: int = 10 # density of integration points relative to grid - -verbose: bool = False - -# ========================================================================= -# RUN SIMULATION -# ========================================================================= - -# -------------------- -# GRID -# -------------------- - -# calculate the grid spacing based on the PPW and F0 -dx: float = c0 / (ppw * source_f0) # [m] - -# compute the size of the grid -# is round_even needed? -Nx: int = round_even(axial_size / dx) -Ny: int = round_even(lateral_size / dx) -Nz: int = Ny - -grid_size_points = Vector([Nx, Ny, Nz]) -grid_spacing_meters = Vector([dx, dx, dx]) - -# create the k-space grid -kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) - -# compute points per temporal period -ppp: int = round(ppw / cfl) - -# compute corresponding time spacing -dt: float = 1.0 / (ppp * source_f0) - -# create the time array using an integer number of points per period -Nt: int = int(np.round(t_end / dt)) -kgrid.setTime(Nt, dt) - -# calculate the actual CFL and PPW -if verbose: - print("PPW = " + str(c0 / (dx * source_f0))) - print("CFL = " + str(c0 * dt / dx)) - -# -------------------- -# SOURCE -# -------------------- - -source = kSource() - -# create time varying source -source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, np.array([source_amp]), np.array([source_phase])) - -# create empty kWaveArray -karray = kWaveArray(bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate) - -# add disc shaped element at one end of the grid -karray.add_disc_element([kgrid.x_vec[0].item(), 0.0, 0.0], source_diam, [0.0, 0.0, 0.0]) - -# assign binary mask -source.p_mask = karray.get_array_binary_mask(kgrid) - -# assign source signals -source.p = karray.get_distributed_source_signal(kgrid, source_sig) - -# -------------------- -# MEDIUM -# -------------------- - -# assign medium properties -medium = kWaveMedium(sound_speed=c0, density=rho0) - -# -------------------- -# SENSOR -# -------------------- - -sensor = kSensor() - -# set sensor mask to record central plane, not including the source point -sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool) -sensor.mask[1:, :, Nz // 2] = True - -# record the pressure -sensor.record = ["p"] - -# record only the final few periods when the field is in steady state -sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1 - -# -------------------- -# SIMULATION -# -------------------- - -simulation_options = SimulationOptions(pml_auto=True, data_recast=True, save_to_disk=True, save_to_disk_exit=False, pml_inside=False) - -execution_options = SimulationExecutionOptions(is_gpu_simulation=True, delete_data=False, verbose_level=2) - -sensor_data = kspaceFirstOrder3D( - medium=deepcopy(medium), - kgrid=deepcopy(kgrid), - source=deepcopy(source), - sensor=deepcopy(sensor), - simulation_options=simulation_options, - execution_options=execution_options, -) - -# extract amplitude from the sensor data -amp, _, _ = extract_amp_phase(sensor_data["p"].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") - -# reshape data -amp = np.reshape(amp, (Nx - 1, Ny), order="F") - -# extract pressure on axis -amp_on_axis = amp[:, Ny // 2] - -# define axis vectors for plotting -x_vec = kgrid.x_vec[1:, :] - kgrid.x_vec[0] -y_vec = kgrid.y_vec - -# ========================================================================= -# ANALYTICAL SOLUTION -# ========================================================================= - -# calculate the wavenumber -k: float = 2.0 * np.pi * source_f0 / c0 - -# define radius and axis -a: float = source_diam / 2.0 -x_max: float = (Nx - 1) * dx -delta_x: float = x_max / 10000.0 -x_ref: float = np.arange(0.0, x_max + delta_x, delta_x, dtype=float) - -# calculate the analytical solution for a piston in an infinite baffle -# for comparison (Eq 5-7.3 in Pierce) -r_ref = np.sqrt(x_ref**2 + a**2) -p_ref = source_amp * np.abs(2.0 * np.sin((k * r_ref - k * x_ref) / 2.0)) - -# get analytical solution at exactly the same points as k-Wave -r = np.sqrt(x_vec**2 + a**2) -p_ref_kw = source_amp * np.abs(2.0 * np.sin((k * r - k * x_vec) / 2.0)) - -# calculate error -L2_error = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=2) -Linf_error = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=np.inf) -# ========================================================================= -# VISUALISATION -# ========================================================================= - -# plot the pressure along the focal axis of the piston -fig1, ax1 = plt.subplots(1, 1) -ax1.plot(1e3 * x_ref, 1e-6 * p_ref, "k-", label="Exact") -ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") -ax1.legend() -ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") -ax1.set_xlim(0.0, 1e3 * axial_size) -ax1.set_ylim(0.0, 1.1 * source_amp * 2e-6) -ax1.grid() - -# plot the source mask (pml is outside the grid in this example) -fig2, ax2 = plt.subplots(1, 1) -ax2.pcolormesh( - 1e3 * np.squeeze(kgrid.y_vec), 1e3 * np.squeeze(kgrid.x_vec), np.flip(source.p_mask[:, :, Nz // 2], axis=0), shading="nearest" -) -ax2.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") - -# plot the pressure field -fig3, ax3 = plt.subplots(1, 1) -ax3.pcolormesh(1e3 * np.squeeze(y_vec), 1e3 * np.squeeze(x_vec), np.flip(amp, axis=1), shading="gouraud") -ax3.set(xlabel="Lateral Position [mm]", ylabel="Axial Position [mm]", title="Pressure Field") -ax3.set_ylim(1e3 * x_vec[-1], 1e3 * x_vec[0]) - -plt.show() +# %% [markdown] +# # Circular Piston Transducer in 3D +# Simulating a circular piston transducer and comparing on-axis pressure with analytical solution. + +# %% +import matplotlib.pyplot as plt +import numpy as np + +from kwave.data import Vector +from kwave.kgrid import kWaveGrid +from kwave.kmedium import kWaveMedium +from kwave.ksensor import kSensor +from kwave.ksource import kSource +from kwave.kspaceFirstOrder import kspaceFirstOrder +from kwave.utils.filters import extract_amp_phase +from kwave.utils.kwave_array import kWaveArray +from kwave.utils.math import round_even +from kwave.utils.signals import create_cw_signals + +# %% Parameters +# material values +c0: float = 1500.0 # sound speed [m/s] +rho0: float = 1000.0 # density [kg/m^3] + +# source parameters +source_f0: float = 1e6 # source frequency [Hz] +source_diam: float = 10e-3 # piston diameter [m] +source_amp: float = 1e6 # source pressure [Pa] +source_phase: float = 0.0 # source pressure [Pa] + +# grid parameters +axial_size: float = 32e-3 # total grid size in the axial dimension [m] +lateral_size: float = 23e-3 # total grid size in the lateral dimension [m] + +# computational parameters +ppw = 3 # number of points per wavelength +t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state) +record_periods: int = 1 # number of periods to record +cfl: float = 0.5 # CFL number +bli_tolerance: float = 0.03 # tolerance for truncation of the off-grid source points +upsampling_rate: int = 10 # density of integration points relative to grid + +verbose: bool = False + +# %% Grid setup +# calculate the grid spacing based on the PPW and F0 +dx: float = c0 / (ppw * source_f0) # [m] + +# compute the size of the grid +# is round_even needed? +Nx: int = round_even(axial_size / dx) +Ny: int = round_even(lateral_size / dx) +Nz: int = Ny + +grid_size_points = Vector([Nx, Ny, Nz]) +grid_spacing_meters = Vector([dx, dx, dx]) + +# create the k-space grid +kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) + +# compute points per temporal period +ppp: int = round(ppw / cfl) + +# compute corresponding time spacing +dt: float = 1.0 / (ppp * source_f0) + +# create the time array using an integer number of points per period +Nt: int = int(np.round(t_end / dt)) +kgrid.setTime(Nt, dt) + +# calculate the actual CFL and PPW +if verbose: + print("PPW = " + str(c0 / (dx * source_f0))) + print("CFL = " + str(c0 * dt / dx)) + +# %% Source, medium, sensor, and simulation +# -------------------- +# SOURCE +# -------------------- + +source = kSource() + +# create time varying source +source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, np.array([source_amp]), np.array([source_phase])) + +# create empty kWaveArray +karray = kWaveArray(bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate) + +# add disc shaped element at one end of the grid +karray.add_disc_element([kgrid.x_vec[0].item(), 0.0, 0.0], source_diam, [0.0, 0.0, 0.0]) + +# assign binary mask +source.p_mask = karray.get_array_binary_mask(kgrid) + +# assign source signals +source.p = karray.get_distributed_source_signal(kgrid, source_sig) + +# -------------------- +# MEDIUM +# -------------------- + +# assign medium properties +medium = kWaveMedium(sound_speed=c0, density=rho0) + +# -------------------- +# SENSOR +# -------------------- + +sensor = kSensor() + +# set sensor mask to record central plane, not including the source point +sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool) +sensor.mask[1:, :, Nz // 2] = True + +# record the pressure +sensor.record = ["p"] + +# record only the final few periods when the field is in steady state +sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1 + +# -------------------- +# SIMULATION +# -------------------- + +# NOTE: pml_inside=False not supported in new API, pml_auto maps to pml_size="auto" +sensor_data = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + pml_size=10, + backend="cpp", + device="gpu", +) + +# extract amplitude from the sensor data +amp, _, _ = extract_amp_phase(sensor_data["p"], 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") + +# reshape data +amp = np.reshape(amp, (Nx - 1, Ny), order="F") + +# extract pressure on axis +amp_on_axis = amp[:, Ny // 2] + +# define axis vectors for plotting +x_vec = kgrid.x_vec[1:, :] - kgrid.x_vec[0] +y_vec = kgrid.y_vec + +# %% Analytical solution +# calculate the wavenumber +k: float = 2.0 * np.pi * source_f0 / c0 + +# define radius and axis +a: float = source_diam / 2.0 +x_max: float = (Nx - 1) * dx +delta_x: float = x_max / 10000.0 +x_ref: float = np.arange(0.0, x_max + delta_x, delta_x, dtype=float) + +# calculate the analytical solution for a piston in an infinite baffle +# for comparison (Eq 5-7.3 in Pierce) +r_ref = np.sqrt(x_ref**2 + a**2) +p_ref = source_amp * np.abs(2.0 * np.sin((k * r_ref - k * x_ref) / 2.0)) + +# get analytical solution at exactly the same points as k-Wave +r = np.sqrt(x_vec**2 + a**2) +p_ref_kw = source_amp * np.abs(2.0 * np.sin((k * r - k * x_vec) / 2.0)) + +# calculate error +L2_error = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=2) +Linf_error = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=np.inf) + +# %% Visualization +# plot the pressure along the focal axis of the piston +fig1, ax1 = plt.subplots(1, 1) +ax1.plot(1e3 * x_ref, 1e-6 * p_ref, "k-", label="Exact") +ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") +ax1.legend() +ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") +ax1.set_xlim(0.0, 1e3 * axial_size) +ax1.set_ylim(0.0, 1.1 * source_amp * 2e-6) +ax1.grid() + +# plot the source mask (pml is outside the grid in this example) +fig2, ax2 = plt.subplots(1, 1) +ax2.pcolormesh( + 1e3 * np.squeeze(kgrid.y_vec), 1e3 * np.squeeze(kgrid.x_vec), np.flip(source.p_mask[:, :, Nz // 2], axis=0), shading="nearest" +) +ax2.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") + +# plot the pressure field +fig3, ax3 = plt.subplots(1, 1) +ax3.pcolormesh(1e3 * np.squeeze(y_vec), 1e3 * np.squeeze(x_vec), np.flip(amp, axis=1), shading="gouraud") +ax3.set(xlabel="Lateral Position [mm]", ylabel="Axial Position [mm]", title="Pressure Field") +ax3.set_ylim(1e3 * x_vec[-1], 1e3 * x_vec[0]) + +plt.show() diff --git a/examples/at_circular_piston_3D/README.md b/examples/at_circular_piston_3D/README.md deleted file mode 100644 index 613eed287..000000000 --- a/examples/at_circular_piston_3D/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Modelling A Circular Plane Piston Transducer Example - -This example models a circular plan piston transducer in 3D. The _on-axis_ pressure is compared with an analytical solution for a piston in an infinite baffle (Eq 5-7.3 in Allan Pierce's book on Acoustics). - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading3) diff --git a/examples/at_circular_piston_3D/at_circular_piston_3D.ipynb b/examples/at_circular_piston_3D/at_circular_piston_3D.ipynb deleted file mode 100644 index 3fa1287c1..000000000 --- a/examples/at_circular_piston_3D/at_circular_piston_3D.ipynb +++ /dev/null @@ -1,455 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "5b6f8e7a-5c7d-4340-8ed4-a04099307131", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install git+https://github.com/waltsims/k-wave-python" - ] - }, - { - "cell_type": "markdown", - "id": "db1bda7e-a820-4eb4-abf9-96ab041675af", - "metadata": {}, - "source": [ - "## Modelling A Circular Piston\n", - "\n", - "This example models a circular piston transducer in 3D. The on-axis pressure is compared with the analytical expression from Pierce's _\"Acoustics: An Introduction to its Physical Principles and Applications\"_\n", - "\n", - "First, define the settings, import the libraries and functions needed" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "32e28e46-df00-4a08-996f-5683470c0e2b", - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "\n", - "from copy import deepcopy\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.filters import extract_amp_phase\n", - "from kwave.utils.kwave_array import kWaveArray\n", - "from kwave.utils.math import round_even\n", - "from kwave.utils.signals import create_cw_signals" - ] - }, - { - "cell_type": "markdown", - "id": "a28dbad5-6ba4-42ef-9e55-715d2383df68", - "metadata": {}, - "source": [ - "The parameters of the system are defined below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67150133-3e02-4fd9-a36d-27a9550fe84f", - "metadata": {}, - "outputs": [], - "source": [ - "# medium parameters\n", - "c0: float = 1500.0 # sound speed [m/s]\n", - "rho0: float = 1000.0 # density [kg/m^3]\n", - "\n", - "# source parameters\n", - "source_f0 = 1.0e6 # source frequency [Hz]\n", - "source_diameter = 10e-3 # bowl aperture diameter [m]\n", - "source_amp = np.array([1.0e6]) # source pressure [Pa]\n", - "source_phase = np.array([0.0]) # source phase [radians]\n", - "\n", - "# grid parameters\n", - "axial_size: float = 32.0e-3 # total grid size in the axial dimension [m]\n", - "lateral_size: float = 23.0e-3 # total grid size in the lateral dimension [m]\n", - "\n", - "# computational parameters\n", - "ppw: int = 3 # number of points per wavelength\n", - "t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state)\n", - "record_periods: int = 1 # number of periods to record\n", - "cfl: float = 0.5 # CFL number\n", - "bli_tolerance: float = 0.03 # tolerance for truncation of the off-grid source points\n", - "upsampling_rate: int = 10 # density of integration points relative to grid\n", - "verbose_level: int = 0 # verbosity of k-wave executable" - ] - }, - { - "cell_type": "markdown", - "id": "26e9fc37-096f-4e2d-af2b-32de0e085cd6", - "metadata": {}, - "source": [ - "## Grid\n", - "\n", - "Construct the grid via the `kgrid` class" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dae95498-e846-4cb1-87ac-dcc684e952bc", - "metadata": {}, - "outputs": [], - "source": [ - "# calculate the grid spacing based on the PPW and F0\n", - "dx: float = c0 / (ppw * source_f0) # [m]\n", - "\n", - "# compute the size of the grid\n", - "Nx: int = round_even(axial_size / dx)\n", - "Ny: int = round_even(lateral_size / dx)\n", - "Nz: int = Ny\n", - "\n", - "grid_size_points = Vector([Nx, Ny, Nz])\n", - "grid_spacing_meters = Vector([dx, dx, dx])\n", - "\n", - "# create the k-space grid\n", - "kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)\n", - "\n", - "# compute points per period\n", - "ppp: int = round(ppw / cfl)\n", - "\n", - "# compute corresponding time spacing\n", - "dt: float = 1.0 / (ppp * source_f0)\n", - "\n", - "# create the time array using an integer number of points per period\n", - "Nt: int = int(np.round(t_end / dt))\n", - "kgrid.setTime(Nt, dt)\n", - "\n", - "# calculate the actual CFL and PPW\n", - "print('points-per-period: ' + str(c0 / (dx * source_f0)) + ' and CFL number: ' + str(c0 * dt / dx))" - ] - }, - { - "cell_type": "markdown", - "id": "b39b121c-cc4d-45ee-9d0a-3cee00c690f5", - "metadata": {}, - "source": [ - "## Source\n", - "\n", - "Define the source, using the `kWaveArray` class and the `add_bowl_element` method along with a continuous wave signal." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d8474039-e3b7-4f09-b76d-6bfaf536bc21", - "metadata": {}, - "outputs": [], - "source": [ - "source = kSource()\n", - "\n", - "# create time varying source\n", - "source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_amp, source_phase)\n", - "\n", - "# create empty kWaveArray\n", - "karray = kWaveArray(bli_tolerance=bli_tolerance,\n", - " upsampling_rate=upsampling_rate,\n", - " single_precision=True)\n", - "\n", - "# add disc shaped element at one end of the grid\n", - "karray.add_disc_element([kgrid.x_vec[0].item(), 0.0, 0.0], source_diameter, [0.0, 0.0, 0.0])\n", - "\n", - "# assign binary mask\n", - "source.p_mask = karray.get_array_binary_mask(kgrid)\n", - "\n", - "# assign source signals\n", - "source.p = karray.get_distributed_source_signal(kgrid, source_sig)" - ] - }, - { - "cell_type": "markdown", - "id": "d9e68ba8-f59f-441d-a86f-bd649571a752", - "metadata": {}, - "source": [ - "## Medium\n", - "\n", - "The medium is water. Neither nonlinearity nor attenuation are considered." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f402c35a-43db-49cc-8947-afd1842f5c63", - "metadata": {}, - "outputs": [], - "source": [ - "# assign medium properties\n", - "medium = kWaveMedium(sound_speed=c0, density=rho0)" - ] - }, - { - "cell_type": "markdown", - "id": "acb68ad1-1f98-4dd5-b000-301386aab42e", - "metadata": {}, - "source": [ - "## Sensor\n", - "\n", - "The sensor class defines what acoustic information is recorded.\n", - "\n", - "A mask records all data except for at the location of the source." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "882f547c-9b94-4da5-8157-3157b983bdf3", - "metadata": {}, - "outputs": [], - "source": [ - "sensor = kSensor()\n", - "\n", - "# set sensor mask to record central plane, not including the source point\n", - "sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool)\n", - "sensor.mask[1:, :, Nz // 2] = True\n", - "\n", - "# record the pressure\n", - "sensor.record = ['p']\n", - "\n", - "# record only the final few periods when the field is in steady state\n", - "sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1" - ] - }, - { - "cell_type": "markdown", - "id": "7df50ac4-3e8e-4cdf-a275-90b54c7e5bea", - "metadata": {}, - "source": [ - "## Simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "66826f6c-b520-463b-a51f-aa4c15ed72a5", - "metadata": {}, - "outputs": [], - "source": [ - "simulation_options = SimulationOptions(pml_auto=True,\n", - " data_recast=True,\n", - " save_to_disk=True,\n", - " save_to_disk_exit=False,\n", - " pml_inside=False)\n", - "\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True,\n", - " delete_data=False,\n", - " verbose_level=0)\n", - "\n", - "sensor_data = kspaceFirstOrder3D(medium=deepcopy(medium),\n", - " kgrid=deepcopy(kgrid),\n", - " source=deepcopy(source),\n", - " sensor=deepcopy(sensor),\n", - " simulation_options=simulation_options,\n", - " execution_options=execution_options)" - ] - }, - { - "cell_type": "markdown", - "id": "77173d6d-bacc-46ee-be2b-e085d4985401", - "metadata": {}, - "source": [ - "## Post-processing\n", - "\n", - "Extract amplitude from the sensor data, using the Fourier transform. The data can be reshaped to match the spatial extents of the domain. The on-axis pressure amplitudes found and axes for plotting defined." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fbd76b31-58b4-4310-bdc6-0b7c6e7818e9", - "metadata": {}, - "outputs": [], - "source": [ - "# extract amplitude from the sensor data\n", - "amp, _, _ = extract_amp_phase(sensor_data['p'].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window='Rectangular')\n", - "\n", - "# reshape data\n", - "amp = np.reshape(amp, (Nx-1, Ny), order='F')\n", - "\n", - "# extract pressure on axis\n", - "amp_on_axis = amp[:, Ny // 2]\n", - "\n", - "# define axis vectors for plotting\n", - "x_vec = kgrid.x_vec[1:, :] - kgrid.x_vec[0]\n", - "y_vec = kgrid.y_vec" - ] - }, - { - "cell_type": "markdown", - "id": "11ec4eed-1349-4ea3-a2e9-61ede65f3702", - "metadata": {}, - "source": [ - "## Analytical Solution\n", - "\n", - "An analytical expression cam be found in Pierce[[1]](#cite_note-1). Given a transdcuer of radius $a$, wavenumber $k= 2 \\pi f / c$, where $f$ is the frequency, speed of sound $c$, and a unit normal vector to transducer surface, $\\hat{v}_n$, the on-axis pressure is given by\n", - "\n", - "$$\n", - "p_{\\mathrm{ref}}(z) = −2 \\, i \\, \\rho \\, c \\, \\hat{v}_n \\, e^{i k \\left( z + \\sqrt{z^2 + a^2} \\right) \\big/ 2} \\sin \\left( \\dfrac{k}{2} \\left( \\sqrt{z^2 + a^2} − z \\right) \\right).\n", - "$$\n", - "\n", - "[[1]](#cite_ref-1) A. D. Pierce, _\"Acoustics: An Introduction to its Physical Principles and Applications\"_ Springer (2019)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d5691bd3-295d-49c1-b815-13f33bc9b952", - "metadata": {}, - "outputs": [], - "source": [ - "# calculate the wavenumber\n", - "k: float = 2.0 * np.pi * source_f0 / c0\n", - "\n", - "# define radius and axis\n", - "a: float = source_diameter / 2.0\n", - "x_max: float = Nx * dx\n", - "delta_x: float = x_max / 10000.0\n", - "x_ref: float = np.arange(0.0, x_max + delta_x, delta_x, dtype=float)\n", - "\n", - "# calculate the analytical solution for a piston in an infinite baffle\n", - "# for comparison (Eq 5-7.3 in Pierce)\n", - "r_ref = np.sqrt(x_ref**2 + a**2)\n", - "p_ref = source_amp * np.abs(2.0 * np.sin((k * r_ref - k * x_ref) / 2.0))\n", - "\n", - "# get analytical solution at exactly the same points as k-Wave\n", - "r_vec = np.sqrt(x_vec**2 + a**2)\n", - "p_ref_kw = source_amp * np.abs(2.0 * np.sin((k * r_vec - k * x_vec) / 2.0))\n", - "\n", - "# calculate error\n", - "L2_error: float = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=2)\n", - "Linf_error: float = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=np.inf)" - ] - }, - { - "cell_type": "markdown", - "id": "373af282-a91a-42c2-82dd-7678de72b1fd", - "metadata": {}, - "source": [ - "## Visualisation\n", - "\n", - "First plot the pressure along the focal axis of the piston" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e38cbfea-115d-4196-b3cc-2585a56e861f", - "metadata": {}, - "outputs": [], - "source": [ - "fig1, ax1 = plt.subplots(1, 1)\n", - "ax1.plot(1e3 * x_ref, 1e-6 * p_ref, 'k-', label='Exact')\n", - "ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, 'b.', label='k-Wave')\n", - "ax1.legend()\n", - "ax1.set(xlabel='Axial Position [mm]',\n", - " ylabel='Pressure [MPa]',\n", - " title='Axial Pressure')\n", - "ax1.set_xlim(0.0, 1e3 * axial_size)\n", - "ax1.set_ylim(0.0, 1.1 * source_amp * 2e-6)\n", - "ax1.grid()" - ] - }, - { - "cell_type": "markdown", - "id": "03150718-35bc-4796-990c-7469b6177159", - "metadata": {}, - "source": [ - "Next plot the source mask (pml is outside the grid in this example). This means getting the grid weights first" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f04f9d92-da57-4627-969d-3c8d96f4c396", - "metadata": {}, - "outputs": [], - "source": [ - "# get grid weights\n", - "grid_weights = karray.get_array_grid_weights(kgrid)\n", - "\n", - "fig2, (ax2a, ax2b) = plt.subplots(1, 2)\n", - "ax2a.pcolormesh(1e3 * np.squeeze(kgrid.y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec),\n", - " np.flip(source.p_mask[:, :, Nz // 2], axis=0),\n", - " shading='nearest')\n", - "ax2a.set(xlabel='y [mm]',\n", - " ylabel='x [mm]',\n", - " title='Source Mask')\n", - "ax2b.pcolormesh(1e3 * np.squeeze(kgrid.y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec),\n", - " np.flip(grid_weights[:, :, Nz // 2], axis=0),\n", - " shading='nearest')\n", - "ax2b.set_xlabel('y [mm]')\n", - "ax2b.set_ylabel('x [mm]')\n", - "_ = ax2b.set_title('Off-Grid Source Weights')\n", - "plt.tight_layout(pad=1.2)" - ] - }, - { - "cell_type": "markdown", - "id": "d51ef784-06c3-4c5f-a478-e6864aa12041", - "metadata": {}, - "source": [ - "Finally, plot the pressure field" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "109c084a-29fb-4180-88e7-e37ecf3b3927", - "metadata": {}, - "outputs": [], - "source": [ - "fig3, ax3 = plt.subplots(1, 1)\n", - "p3 = ax3.pcolormesh(1.0e3 * np.squeeze(y_vec),\n", - " 1.0e3 * np.squeeze(x_vec),\n", - " np.flip(amp, axis=1) / 1.0e6,\n", - " shading='gouraud')\n", - "ax3.set(xlabel='Lateral Position [mm]',\n", - " ylabel='Axial Position [mm]',\n", - " title='Pressure Field')\n", - "ax3.set_ylim(1.0e3 * x_vec[-1], 1.0e3 * x_vec[0])\n", - "cbar3 = fig3.colorbar(p3, ax=ax3)\n", - "_ = cbar3.ax.set_title('[MPa]', fontsize='small')" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/at_circular_piston_AS/at_circular_piston_AS.py b/examples/at_circular_piston_AS.py similarity index 79% rename from examples/at_circular_piston_AS/at_circular_piston_AS.py rename to examples/at_circular_piston_AS.py index b29ed0095..652b77b56 100644 --- a/examples/at_circular_piston_AS/at_circular_piston_AS.py +++ b/examples/at_circular_piston_AS.py @@ -1,233 +1,212 @@ -""" -Modelling A Circular Plane Piston Transducer Assuming Axisymmetry Example - -This example models a circular piston transducer assuming axisymmetry. -The on-axis pressure is compared with the analytical expression from [1]. -Compared to the 3D simulation, a lower CFL (which gives a smaller time -step) is used, as the k-space correction for the axisymmetric code is not -exact in the radial direction. -[1] A. D. Pierce, Acoustics: An Introduction to its Physical Principles -and Applications. New York: Acoustical Society of America, 1989. -""" - -from copy import deepcopy - -import matplotlib.pyplot as plt -import numpy as np - -from kwave.data import Vector -from kwave.kgrid import kWaveGrid -from kwave.kmedium import kWaveMedium -from kwave.ksensor import kSensor -from kwave.ksource import kSource -from kwave.kspaceFirstOrderAS import kspaceFirstOrderASC -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions, SimulationType -from kwave.utils.filters import extract_amp_phase -from kwave.utils.kwave_array import kWaveArray -from kwave.utils.math import round_even -from kwave.utils.signals import create_cw_signals - -# medium parameters -c0 = 1500.0 # sound speed [m/s] -rho0 = 1000.0 # density [kg/m^3] - -# piston diameter [m] -source_diam = 10e-3 - -# source frequency [Hz] -source_f0 = 1e6 - -# source pressure [Pa] -source_mag = np.array([1e6]) - -# phase [rad] -source_phase = np.array([0.0]) - -# grid parameters -axial_size = 32e-3 # total grid size in the axial dimension [m] -lateral_size = 8e-3 # total grid size in the lateral dimension [m] - -# computational parameters -ppw = 4 # number of points per wavelength -t_end = 40e-6 # total compute time [s] (this must be long enough to reach steady state) -record_periods = 1 # number of periods to record -cfl = 0.05 # CFL number -bli_tolerance = 0.05 # tolerance for truncation of the off-grid source points -upsampling_rate = 10 # density of integration points relative to grid - -# ========================================================================= -# RUN SIMULATION -# ========================================================================= - -# -------------------- -# GRID -# -------------------- - -# grid resolution -dx = c0 / (ppw * source_f0) # [m] - -# compute the size of the grid -Nx = round_even(axial_size / dx) -Ny = round_even(lateral_size / dx) - -grid_size_points = Vector([Nx, Ny]) -grid_spacing_meters = Vector([dx, dx]) - -# create the k-space grid -kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) - -# compute points per period -ppp = round(ppw / cfl) - -# compute time step -dt = 1.0 / (ppp * source_f0) - -# create the time array using an integer number of points per period -Nt = round(t_end / dt) -kgrid.setTime(Nt, dt) - - -# -------------------- -# SOURCE -# -------------------- - -# create time varying continuous wave source -source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_mag, source_phase) - -# create empty kWaveArray this specifies the transducer properties in -# axisymmetric coordinate system -karray = kWaveArray(axisymmetric=True, bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate, single_precision=True) - -# add line shaped element for transducer -karray.add_line_element([kgrid.x_vec[0].item(), -source_diam / 2.0], [kgrid.x_vec[0].item(), source_diam / 2.0]) - - -# make a source object -source = kSource() - -# assign binary mask using the karray -source.p_mask = karray.get_array_binary_mask(kgrid) - -# assign source pressure output in time -source.p = karray.get_distributed_source_signal(kgrid, source_sig) - - -# -------------------- -# MEDIUM -# -------------------- - -# water -medium = kWaveMedium(sound_speed=c0, density=rho0) - -# -------------------- -# SENSOR -# -------------------- - -sensor = kSensor() - -# set sensor mask to record central plane, not including the source point -# sensor.mask = np.zeros((Nx, Ny), dtype=bool) -# sensor.mask[1:, :] = True - -sensor.mask = np.ones((Nx, Ny), dtype=bool) - -# set the record type: record the pressure waveform -sensor.record = ["p"] - -# record only the final few periods when the field is in steady state -sensor.record_start_index = kgrid.Nt - record_periods * ppp + 1 - -# ========================================================================= -# DEFINE THE SIMULATION PARAMETERS -# ========================================================================= - - -# options for writing to file, but not doing simulations -simulation_options = SimulationOptions( - simulation_type=SimulationType.AXISYMMETRIC, pml_auto=True, save_to_disk=True, save_to_disk_exit=False, pml_inside=False -) - -execution_options = SimulationExecutionOptions(is_gpu_simulation=False, delete_data=False, verbose_level=2) - -# ========================================================================= -# RUN THE SIMULATION -# ========================================================================= - -sensor_data = kspaceFirstOrderASC( - medium=medium, - kgrid=kgrid, - source=deepcopy(source), - sensor=sensor, - simulation_options=simulation_options, - execution_options=execution_options, -) - -# extract amplitude from the sensor data -amp, _, _ = extract_amp_phase(sensor_data["p"].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") - -# reshape data -amp = np.reshape(amp, (Nx, Ny), order="F") - -# extract pressure on axis -amp_on_axis = amp[:, 0] - -# define axis vectors for plotting -yvec = np.squeeze(kgrid.y_vec) - kgrid.y_vec[0].item() -y_vec = 1e3 * np.hstack((-np.flip(yvec)[:-1], yvec)) -x_vec = 1e3 * np.squeeze(kgrid.x_vec[:, :] - kgrid.x_vec[0]) - -# ========================================================================= -# ANALYTICAL SOLUTION -# ========================================================================= - -# calculate the wavenumber -k: float = 2.0 * np.pi * source_f0 / c0 - -# define radius and axis -a: float = source_diam / 2.0 -x_max: float = (Nx - 1) * dx -delta_x: float = x_max / 10000.0 -x_ref: float = np.arange(0.0, x_max + delta_x, delta_x, dtype=float) - -# calculate the analytical solution for a piston in an infinite baffle -# for comparison (Eq 5-7.3 in Pierce) -r_ref = np.sqrt(x_ref**2 + a**2) -p_ref = source_mag[0] * np.abs(2.0 * np.sin((k * r_ref - k * x_ref) / 2.0)) - -# # get analytical solution at exactly the same points as k-Wave -# r = np.sqrt(x_vec**2 + a**2) -# p_ref_kw = source_mag[0] * np.abs(2.0 * np.sin((k * r - k * x_vec) / 2.0)) - -# # calculate error -# L2_error = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=2) -# Linf_error = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=np.inf) - -# ========================================================================= -# VISUALISATION -# ========================================================================= - -data = np.hstack((np.fliplr(amp[:, :-1]), amp)) / 1e6 -sp = np.hstack((np.fliplr(source.p_mask[:, :])[:, :-1], source.p_mask)) - -# plot the pressure along the focal axis of the piston -fig1, ax1 = plt.subplots(1, 1) -ax1.plot(1e3 * x_ref, 1e-6 * p_ref, "k-", label="Exact") -ax1.plot(x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") -ax1.legend() -ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") -ax1.grid() - -# plot the source mask -fig2, ax2 = plt.subplots(1, 1) -ax2.pcolormesh(y_vec, x_vec, sp[:, :], shading="nearest") -ax2.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") -ax2.invert_yaxis() - -fig3, ax3 = plt.subplots(1, 1) -im3 = ax3.pcolormesh(y_vec, x_vec, data, shading="gouraud") -cbar3 = fig3.colorbar(im3, ax=ax3) -_ = cbar3.ax.set_title("[MPa]", fontsize="small") -ax3.invert_yaxis() - -plt.show() +# %% [markdown] +# # Circular Piston Transducer (Axisymmetric) +# Modelling a circular piston transducer assuming axisymmetry, compared with analytical solution. + +# %% +""" +Modelling A Circular Plane Piston Transducer Assuming Axisymmetry Example + +This example models a circular piston transducer assuming axisymmetry. +The on-axis pressure is compared with the analytical expression from [1]. +Compared to the 3D simulation, a lower CFL (which gives a smaller time +step) is used, as the k-space correction for the axisymmetric code is not +exact in the radial direction. +[1] A. D. Pierce, Acoustics: An Introduction to its Physical Principles +and Applications. New York: Acoustical Society of America, 1989. +""" + +import matplotlib.pyplot as plt +import numpy as np + +from kwave.data import Vector +from kwave.kgrid import kWaveGrid +from kwave.kmedium import kWaveMedium +from kwave.ksensor import kSensor +from kwave.ksource import kSource +from kwave.kspaceFirstOrderAS import kspaceFirstOrderASC +from kwave.options.simulation_execution_options import SimulationExecutionOptions +from kwave.options.simulation_options import SimulationOptions +from kwave.utils.filters import extract_amp_phase +from kwave.utils.kwave_array import kWaveArray +from kwave.utils.math import round_even +from kwave.utils.signals import create_cw_signals + +# %% Parameters +# medium parameters +c0 = 1500.0 # sound speed [m/s] +rho0 = 1000.0 # density [kg/m^3] + +# piston diameter [m] +source_diam = 10e-3 + +# source frequency [Hz] +source_f0 = 1e6 + +# source pressure [Pa] +source_mag = np.array([1e6]) + +# phase [rad] +source_phase = np.array([0.0]) + +# grid parameters +axial_size = 32e-3 # total grid size in the axial dimension [m] +lateral_size = 8e-3 # total grid size in the lateral dimension [m] + +# computational parameters +ppw = 4 # number of points per wavelength +t_end = 40e-6 # total compute time [s] (this must be long enough to reach steady state) +record_periods = 1 # number of periods to record +cfl = 0.05 # CFL number +bli_tolerance = 0.05 # tolerance for truncation of the off-grid source points +upsampling_rate = 10 # density of integration points relative to grid + +# %% Grid setup +# grid resolution +dx = c0 / (ppw * source_f0) # [m] + +# compute the size of the grid +Nx = round_even(axial_size / dx) +Ny = round_even(lateral_size / dx) + +grid_size_points = Vector([Nx, Ny]) +grid_spacing_meters = Vector([dx, dx]) + +# create the k-space grid +kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) + +# compute points per period +ppp = round(ppw / cfl) + +# compute time step +dt = 1.0 / (ppp * source_f0) + +# create the time array using an integer number of points per period +Nt = round(t_end / dt) +kgrid.setTime(Nt, dt) + +# %% Source, medium, sensor, and simulation +# -------------------- +# SOURCE +# -------------------- + +# create time varying continuous wave source +source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_mag, source_phase) + +# create empty kWaveArray this specifies the transducer properties in +# axisymmetric coordinate system +karray = kWaveArray(axisymmetric=True, bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate, single_precision=True) + +# add line shaped element for transducer +karray.add_line_element([kgrid.x_vec[0].item(), -source_diam / 2.0], [kgrid.x_vec[0].item(), source_diam / 2.0]) + + +# make a source object +source = kSource() + +# assign binary mask using the karray +source.p_mask = karray.get_array_binary_mask(kgrid) + +# assign source pressure output in time +source.p = karray.get_distributed_source_signal(kgrid, source_sig) + + +# -------------------- +# MEDIUM +# -------------------- + +# water +medium = kWaveMedium(sound_speed=c0, density=rho0) + +# -------------------- +# SENSOR +# -------------------- + +sensor = kSensor() + +# set sensor mask to record central plane, not including the source point +# sensor.mask = np.zeros((Nx, Ny), dtype=bool) +# sensor.mask[1:, :] = True + +sensor.mask = np.ones((Nx, Ny), dtype=bool) + +# set the record type: record the pressure waveform +sensor.record = ["p"] + +# record only the final few periods when the field is in steady state +sensor.record_start_index = kgrid.Nt - record_periods * ppp + 1 + +# NOTE: Axisymmetric simulations require the legacy kspaceFirstOrderASC API +# until the new kspaceFirstOrder() supports the axisymmetric flag. +simulation_options = SimulationOptions(save_to_disk=True, pml_inside=False, data_cast="single") +execution_options = SimulationExecutionOptions(is_gpu_simulation=False) +sensor_data = kspaceFirstOrderASC( + kgrid=kgrid, + medium=medium, + source=source, + sensor=sensor, + simulation_options=simulation_options, + execution_options=execution_options, +) + +# extract amplitude from the sensor data +amp, _, _ = extract_amp_phase(sensor_data["p"].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") + +# reshape data +amp = np.reshape(amp, (Nx, Ny), order="F") + +# extract pressure on axis +amp_on_axis = amp[:, 0] + +# define axis vectors for plotting +yvec = np.squeeze(kgrid.y_vec) - kgrid.y_vec[0].item() +y_vec = 1e3 * np.hstack((-np.flip(yvec)[:-1], yvec)) +x_vec = 1e3 * np.squeeze(kgrid.x_vec[:, :] - kgrid.x_vec[0]) + +# %% Analytical solution +# calculate the wavenumber +k: float = 2.0 * np.pi * source_f0 / c0 + +# define radius and axis +a: float = source_diam / 2.0 +x_max: float = (Nx - 1) * dx +delta_x: float = x_max / 10000.0 +x_ref: float = np.arange(0.0, x_max + delta_x, delta_x, dtype=float) + +# calculate the analytical solution for a piston in an infinite baffle +# for comparison (Eq 5-7.3 in Pierce) +r_ref = np.sqrt(x_ref**2 + a**2) +p_ref = source_mag[0] * np.abs(2.0 * np.sin((k * r_ref - k * x_ref) / 2.0)) + +# # get analytical solution at exactly the same points as k-Wave +# r = np.sqrt(x_vec**2 + a**2) +# p_ref_kw = source_mag[0] * np.abs(2.0 * np.sin((k * r - k * x_vec) / 2.0)) + +# # calculate error +# L2_error = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=2) +# Linf_error = 100 * np.linalg.norm(p_ref_kw - amp_on_axis, ord=np.inf) + +# %% Visualization +data = np.hstack((np.fliplr(amp[:, :-1]), amp)) / 1e6 +sp = np.hstack((np.fliplr(source.p_mask[:, :])[:, :-1], source.p_mask)) + +# plot the pressure along the focal axis of the piston +fig1, ax1 = plt.subplots(1, 1) +ax1.plot(1e3 * x_ref, 1e-6 * p_ref, "k-", label="Exact") +ax1.plot(x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") +ax1.legend() +ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") +ax1.grid() + +# plot the source mask +fig2, ax2 = plt.subplots(1, 1) +ax2.pcolormesh(y_vec, x_vec, sp[:, :], shading="nearest") +ax2.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") +ax2.invert_yaxis() + +fig3, ax3 = plt.subplots(1, 1) +im3 = ax3.pcolormesh(y_vec, x_vec, data, shading="gouraud") +cbar3 = fig3.colorbar(im3, ax=ax3) +_ = cbar3.ax.set_title("[MPa]", fontsize="small") +ax3.invert_yaxis() + +plt.show() diff --git a/examples/at_circular_piston_AS/README.md b/examples/at_circular_piston_AS/README.md deleted file mode 100644 index c56788f83..000000000 --- a/examples/at_circular_piston_AS/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Modelling An Axisymmetric Circular Plane Piston Transducer Example - -This example models an axisymmetric circular plan piston transducer. The _on-axis_ pressure is compared with an analytical solution for a piston in an infinite baffle (Eq 5-7.3 in Allan Pierce's book on Acoustics). - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading4) \ No newline at end of file diff --git a/examples/at_circular_piston_AS/at_circular_piston_AS.ipynb b/examples/at_circular_piston_AS/at_circular_piston_AS.ipynb deleted file mode 100644 index 6247925fe..000000000 --- a/examples/at_circular_piston_AS/at_circular_piston_AS.ipynb +++ /dev/null @@ -1,383 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install git+https://github.com/waltsims/k-wave-python" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from copy import deepcopy\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrderAS import kspaceFirstOrderASC\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions, SimulationType\n", - "from kwave.utils.filters import extract_amp_phase\n", - "from kwave.utils.kwave_array import kWaveArray\n", - "from kwave.utils.mapgen import focused_bowl_oneil\n", - "from kwave.utils.math import round_even\n", - "from kwave.utils.signals import create_cw_signals" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Set parameters" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# medium parameters\n", - "c0: float = 1500.0 # sound speed [m/s]\n", - "rho0: float = 1000.0 # density [kg/m^3]\n", - "\n", - "# source parameters\n", - "source_f0 = 1.0e6 # source frequency [Hz]\n", - "source_roc = 30e-3 # bowl radius of curvature [m]\n", - "source_diameter = 30e-3 # bowl aperture diameter [m]\n", - "source_amp = np.array([1.0e6]) # source pressure [Pa]\n", - "source_phase = np.array([0.0]) # source phase [radians]\n", - "\n", - "# grid parameters\n", - "axial_size: float = 50.0e-3 # total grid size in the axial dimension [m]\n", - "lateral_size: float = 45.0e-3 # total grid size in the lateral dimension [m]\n", - "\n", - "# computational parameters\n", - "ppw: int = 3 # number of points per wavelength\n", - "t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state)\n", - "record_periods: int = 1 # number of periods to record\n", - "cfl: float = 0.05 # CFL number\n", - "source_x_offset: int = 20 # grid points to offset the source\n", - "bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points\n", - "upsampling_rate: int = 10 # density of integration points relative to grid" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Set grid" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# calculate the grid spacing based on the PPW and F0\n", - "dx: float = c0 / (ppw * source_f0) # [m]\n", - "\n", - "# compute the size of the grid\n", - "Nx: int= round_even(axial_size / dx) + source_x_offset\n", - "Ny: int = round_even(lateral_size / dx)\n", - "\n", - "grid_size_points = Vector([Nx, Ny])\n", - "grid_spacing_meters = Vector([dx, dx])\n", - "\n", - "# create the k-space grid\n", - "kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)\n", - "\n", - "# compute points per temporal period\n", - "ppp: int = round(ppw / cfl)\n", - "\n", - "# compute corresponding time spacing\n", - "dt: float = 1.0 / (ppp * source_f0)\n", - "\n", - "# create the time array using an integer number of points per period\n", - "Nt: int = round(t_end / dt)\n", - "kgrid.setTime(Nt, dt)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Define source" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "source = kSource()\n", - "\n", - "# create time varying source\n", - "source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_amp, source_phase)\n", - "\n", - "# set arc position and orientation\n", - "arc_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0]\n", - "focus_pos = [kgrid.x_vec[-1].item(), 0]\n", - "\n", - "# create empty kWaveArray\n", - "karray = kWaveArray(axisymmetric=True,\n", - " bli_tolerance=bli_tolerance,\n", - " upsampling_rate=upsampling_rate,\n", - " single_precision=True)\n", - "\n", - "# add bowl shaped element\n", - "karray.add_arc_element(arc_pos, source_roc, source_diameter, focus_pos)\n", - "\n", - "# assign binary mask\n", - "source.p_mask = karray.get_array_binary_mask(kgrid)\n", - "\n", - "# assign source signals\n", - "source.p = karray.get_distributed_source_signal(kgrid, source_sig)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Set up medium" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "medium = kWaveMedium(sound_speed=c0, density=rho0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Set up sensor" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sensor = kSensor()\n", - "\n", - "# set sensor mask to record central plane, not including the source point\n", - "sensor.mask = np.zeros((Nx, Ny), dtype=bool)\n", - "sensor.mask[(source_x_offset + 1):, :] = True\n", - "\n", - "# record the pressure\n", - "sensor.record = ['p']\n", - "\n", - "# record only the final few periods when the field is in steady state\n", - "sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Define simulation options" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "simulation_options = SimulationOptions(\n", - " simulation_type=SimulationType.AXISYMMETRIC,\n", - " data_cast='single',\n", - " data_recast=False,\n", - " save_to_disk=True,\n", - " save_to_disk_exit=False,\n", - " pml_inside=False)\n", - "\n", - "execution_options = SimulationExecutionOptions(\n", - " is_gpu_simulation=False,\n", - " delete_data=False,\n", - " verbose_level=2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Run simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sensor_data = kspaceFirstOrderASC(medium=deepcopy(medium),\n", - " kgrid=deepcopy(kgrid),\n", - " source=deepcopy(source),\n", - " sensor=deepcopy(sensor),\n", - " simulation_options=simulation_options,\n", - " execution_options=execution_options)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Post-processing" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# extract amplitude from the sensor data\n", - "amp, _, _ = extract_amp_phase(sensor_data['p'].T, 1.0 / kgrid.dt, source_f0,\n", - " dim=1, fft_padding=1, window='Rectangular')\n", - "\n", - "# reshape data\n", - "amp = np.reshape(amp, (Nx - (source_x_offset+1), Ny), order='F')\n", - "\n", - "# extract pressure on axis\n", - "amp_on_axis = amp[:, 0]\n", - "\n", - "# define axis vectors for plotting\n", - "x_vec = np.squeeze(kgrid.x_vec[(source_x_offset + 1):, :] - kgrid.x_vec[source_x_offset])\n", - "y_vec = kgrid.y_vec\n", - "\n", - "# =========================================================================\n", - "# ANALYTICAL SOLUTION\n", - "# =========================================================================\n", - "\n", - "# calculate the wavenumber\n", - "knumber = 2.0 * np.pi * source_f0 / c0\n", - "\n", - "# define radius and axis\n", - "x_max = (Nx-1) * dx\n", - "delta_x = x_max / 10000.0\n", - "x_ref = np.arange(0.0, x_max + delta_x, delta_x)\n", - "\n", - "# calculate analytical solution\n", - "p_ref_axial, _, _ = focused_bowl_oneil(source_roc,\n", - " source_diameter,\n", - " source_amp[0] / (c0 * rho0),\n", - " source_f0,\n", - " c0,\n", - " rho0,\n", - " axial_positions=x_ref)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Plot solution" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot the pressure along the focal axis of the piston\n", - "fig1, ax1 = plt.subplots(1, 1)\n", - "ax1.plot(1e3 * x_ref, 1e-6 * p_ref_axial, 'k-', label='Exact')\n", - "ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, 'b.', label='k-Wave')\n", - "ax1.legend()\n", - "ax1.set(xlabel='Axial Position [mm]',\n", - " ylabel='Pressure [MPa]',\n", - " title='Axial Pressure')\n", - "ax1.set_xlim(0.0, 1e3 * axial_size)\n", - "ax1.set_ylim(0.0, 20)\n", - "ax1.grid()\n", - "\n", - "# get grid weights\n", - "grid_weights = karray.get_array_grid_weights(kgrid)\n", - "\n", - "# define axis vectors for plotting\n", - "yvec = np.squeeze(kgrid.y_vec) - kgrid.y_vec[0].item()\n", - "y_vec = np.hstack((-np.flip(yvec)[:-1], yvec))\n", - "x_vec = np.squeeze(kgrid.x_vec[(source_x_offset + 1):, :] - kgrid.x_vec[source_x_offset])\n", - "\n", - "data = np.hstack((np.fliplr(amp[:, :-1]), amp)) / 1e6\n", - "sp = np.hstack((np.fliplr(source.p_mask[:, :-1]), source.p_mask))\n", - "gw = np.hstack((np.fliplr(grid_weights[:, :-1]), grid_weights))\n", - "\n", - "fig3, (ax3a, ax3b) = plt.subplots(1, 2)\n", - "ax3a.pcolormesh(1e3 * np.squeeze(y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec[:, :] - kgrid.x_vec[source_x_offset]),\n", - " sp,\n", - " shading='gouraud')\n", - "ax3a.set(xlabel='y [mm]',\n", - " ylabel='x [mm]',\n", - " title='Source Mask')\n", - "ax3b.pcolormesh(1e3 * np.squeeze(y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec[:, :] - kgrid.x_vec[source_x_offset]),\n", - " gw,\n", - " shading='gouraud')\n", - "ax3b.set(xlabel='y [mm]',\n", - " ylabel='x [mm]',\n", - " title='Off-Grid Source Weights')\n", - "fig3.tight_layout(pad=1.2)\n", - "\n", - "# plot the pressure field\n", - "fig4, ax4 = plt.subplots(1, 1)\n", - "ax4.pcolormesh(1e3 * np.squeeze(y_vec),\n", - " 1e3 * np.squeeze(x_vec),\n", - " data,\n", - " shading='gouraud')\n", - "ax4.set(xlabel='Lateral Position [mm]',\n", - " ylabel='Axial Position [mm]',\n", - " title='Pressure Field')\n", - "ax4.invert_yaxis()\n", - "\n", - "# show figures\n", - "plt.show()" - ] - } - ], - "metadata": { - "colab": { - "authorship_tag": "ABX9TyOTl1BLKuzg6aBmvj8vKKSi", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.py b/examples/at_focused_annular_array_3D.py similarity index 78% rename from examples/at_focused_annular_array_3D/at_focused_annular_array_3D.py rename to examples/at_focused_annular_array_3D.py index 3f019f40c..4093601b9 100644 --- a/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.py +++ b/examples/at_focused_annular_array_3D.py @@ -1,214 +1,202 @@ -from copy import deepcopy - -import matplotlib.pyplot as plt -import numpy as np - -from kwave.data import Vector -from kwave.kgrid import kWaveGrid -from kwave.kmedium import kWaveMedium -from kwave.ksensor import kSensor -from kwave.ksource import kSource -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions -from kwave.utils.filters import extract_amp_phase -from kwave.utils.kwave_array import kWaveArray -from kwave.utils.mapgen import focused_annulus_oneil -from kwave.utils.math import round_even -from kwave.utils.signals import create_cw_signals - -verbose: bool = False - -# medium parameters -c0: float = 1500.0 # sound speed [m/s] -rho0: float = 1000.0 # density [kg/m^3] - -# source parameters -source_f0 = 1.0e6 # source frequency [Hz] -source_roc = 30e-3 # bowl radius of curvature [m] -source_amp = np.array([0.5e6, 1e6, 0.75e6]) # source pressure [Pa] -source_phase = np.deg2rad(np.array([0.0, 10.0, 20.0])) # source phase [radians] - -# aperture diameters of the elements given an inner, outer pairs [m] -diameters = np.array([[0.0, 5.0], [10.0, 15.0], [20.0, 25.0]]) * 1e-3 -diameters = diameters.tolist() - -# grid parameters -axial_size: float = 40e-3 # total grid size in the axial dimension [m] -lateral_size: float = 45e-3 # total grid size in the lateral dimension [m] - -# computational parameters -ppw: int = 3 # number of points per wavelength -t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state) -record_periods: int = 1 # number of periods to record -cfl: float = 0.5 # CFL number -source_x_offset: int = 20 # grid points to offset the source -bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points -upsampling_rate: int = 10 # density of integration points relative to grid -verbose_level: int = 0 # verbosity of k-wave executable - -# ========================================================================= -# RUN SIMULATION -# ========================================================================= - -# -------------------- -# GRID -# -------------------- - -# calculate the grid spacing based on the points per wavelength and the frequency -dx: float = c0 / (ppw * source_f0) # [m] - -# compute the size of the grid -Nx: int = round_even(axial_size / dx) + source_x_offset -Ny: int = round_even(lateral_size / dx) -Nz: int = Ny - -# create the k-space grid -grid_size_points = Vector([Nx, Ny, Nz]) -grid_spacing_meters = Vector([dx, dx, dx]) -kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) - -# compute points per temporal period. -ppp: int = round(ppw / cfl) - -# compute corresponding time spacing -dt = 1.0 / (ppp * source_f0) - -# create the time array using an integer number of points per period -Nt = int(np.round(t_end / dt)) -kgrid.setTime(Nt, dt) - -# calculate the actual CFL and PPW -if verbose: - print("PPW = " + str(c0 / (dx * source_f0))) - print("CFL = " + str(c0 * dt / dx)) - -# -------------------- -# SOURCE -# -------------------- - -# create empty kSource -source = kSource() - -# create time varying source based on time, frequency, amplitude and phase -source_signal = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_amp, source_phase) - -# set bowl position and orientation -bowl_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0, 0] -focus_pos = [kgrid.x_vec[-1].item(), 0, 0] - -# create empty kWaveArray -karray = kWaveArray(bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate, single_precision=True) - -# add bowl shaped element to array -karray.add_annular_array(bowl_pos, source_roc, diameters, focus_pos) - -# assign binary mask to source, based on array elements and the grid -source.p_mask = karray.get_array_binary_mask(kgrid) - -# assign source signals, based on array elements, grid and signal -source.p = karray.get_distributed_source_signal(kgrid, source_signal) - -# -------------------- -# MEDIUM -# -------------------- - -# assign medium properties -medium = kWaveMedium(sound_speed=c0, density=rho0) - -# -------------------- -# SENSOR -# -------------------- - -sensor = kSensor() - -# set sensor mask to record central plane, not including the source point -sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool) -sensor.mask[(source_x_offset + 1) :, :, Nz // 2] = True - -# record the pressure -sensor.record = ["p"] - -# record only the final few periods when the field is in steady state -sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1 - -# -------------------- -# SIMULATION -# -------------------- - -simulation_options = SimulationOptions(pml_auto=True, data_recast=True, save_to_disk=True, save_to_disk_exit=False, pml_inside=False) - -execution_options = SimulationExecutionOptions(is_gpu_simulation=True, delete_data=False, verbose_level=2) - -sensor_data = kspaceFirstOrder3D( - medium=deepcopy(medium), - kgrid=deepcopy(kgrid), - source=deepcopy(source), - sensor=deepcopy(sensor), - simulation_options=simulation_options, - execution_options=execution_options, -) - -# extract amplitude from the sensor data -amp, _, _ = extract_amp_phase(sensor_data["p"].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") - -# reshape data -amp = np.reshape(amp, (Nx - (source_x_offset + 1), Ny), order="F") - -# extract pressure on axis -amp_on_axis = amp[:, Ny // 2] - -# define axis vectors for plotting -x_vec = kgrid.x_vec[source_x_offset + 1 :, :] - kgrid.x_vec[source_x_offset] -y_vec = kgrid.y_vec - -# ========================================================================= -# ANALYTICAL SOLUTION -# ========================================================================= - -p_axial = focused_annulus_oneil( - source_roc, np.asarray(diameters).T, source_amp / (c0 * rho0), source_phase, source_f0, c0, rho0, np.squeeze(x_vec) -) - -# ========================================================================= -# VISUALISATION -# ========================================================================= - -# plot the pressure along the focal axis of the piston -fig1, ax1 = plt.subplots(1, 1) -ax1.plot(1e3 * x_vec, 1e-6 * p_axial, "k-", label="Exact") -ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") -ax1.legend() -ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") -ax1.set_xlim(0.0, 1e3 * axial_size) -ax1.grid() - -# plot the source mask (pml is outside the grid in this example) - -# get grid weights -grid_weights = karray.get_array_grid_weights(kgrid) - -fig2, (ax2a, ax2b) = plt.subplots(1, 2) -ax2a.pcolormesh( - 1e3 * np.squeeze(kgrid.y_vec), - 1e3 * np.squeeze(kgrid.x_vec), - np.flip(source.p_mask[:, :, Nz // 2], axis=0), - shading="nearest", -) -ax2a.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") -ax2b.pcolormesh( - 1e3 * np.squeeze(kgrid.y_vec), - 1e3 * np.squeeze(kgrid.x_vec), - np.flip(grid_weights[:, :, Nz // 2], axis=0), - shading="nearest", -) -ax2b.set(xlabel="y [mm]", ylabel="x [mm]", title="Off-Grid Source Weights") -plt.tight_layout(pad=1.2) - -# plot the pressure field -fig3, ax3 = plt.subplots(1, 1) -ax3.pcolormesh(1e3 * np.squeeze(y_vec), 1e3 * np.squeeze(x_vec), np.flip(amp, axis=1), shading="gouraud") -ax3.set(xlabel="Lateral Position [mm]", ylabel="Axial Position [mm]", title="Pressure Field") -ax3.set_ylim(1e3 * x_vec[-1], 1e3 * x_vec[0]) - -plt.show() +# %% [markdown] +# # Focused Annular Array in 3D +# Simulating a focused annular array transducer and comparing with analytical O'Neil solution. + +# %% +import matplotlib.pyplot as plt +import numpy as np + +from kwave.data import Vector +from kwave.kgrid import kWaveGrid +from kwave.kmedium import kWaveMedium +from kwave.ksensor import kSensor +from kwave.ksource import kSource +from kwave.kspaceFirstOrder import kspaceFirstOrder +from kwave.utils.filters import extract_amp_phase +from kwave.utils.kwave_array import kWaveArray +from kwave.utils.mapgen import focused_annulus_oneil +from kwave.utils.math import round_even +from kwave.utils.signals import create_cw_signals + +# %% Parameters +verbose: bool = False + +# medium parameters +c0: float = 1500.0 # sound speed [m/s] +rho0: float = 1000.0 # density [kg/m^3] + +# source parameters +source_f0 = 1.0e6 # source frequency [Hz] +source_roc = 30e-3 # bowl radius of curvature [m] +source_amp = np.array([0.5e6, 1e6, 0.75e6]) # source pressure [Pa] +source_phase = np.deg2rad(np.array([0.0, 10.0, 20.0])) # source phase [radians] + +# aperture diameters of the elements given an inner, outer pairs [m] +diameters = np.array([[0.0, 5.0], [10.0, 15.0], [20.0, 25.0]]) * 1e-3 +diameters = diameters.tolist() + +# grid parameters +axial_size: float = 40e-3 # total grid size in the axial dimension [m] +lateral_size: float = 45e-3 # total grid size in the lateral dimension [m] + +# computational parameters +ppw: int = 3 # number of points per wavelength +t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state) +record_periods: int = 1 # number of periods to record +cfl: float = 0.5 # CFL number +source_x_offset: int = 20 # grid points to offset the source +bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points +upsampling_rate: int = 10 # density of integration points relative to grid +verbose_level: int = 0 # verbosity of k-wave executable + +# %% Grid setup +# calculate the grid spacing based on the points per wavelength and the frequency +dx: float = c0 / (ppw * source_f0) # [m] + +# compute the size of the grid +Nx: int = round_even(axial_size / dx) + source_x_offset +Ny: int = round_even(lateral_size / dx) +Nz: int = Ny + +# create the k-space grid +grid_size_points = Vector([Nx, Ny, Nz]) +grid_spacing_meters = Vector([dx, dx, dx]) +kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) + +# compute points per temporal period. +ppp: int = round(ppw / cfl) + +# compute corresponding time spacing +dt = 1.0 / (ppp * source_f0) + +# create the time array using an integer number of points per period +Nt = int(np.round(t_end / dt)) +kgrid.setTime(Nt, dt) + +# calculate the actual CFL and PPW +if verbose: + print("PPW = " + str(c0 / (dx * source_f0))) + print("CFL = " + str(c0 * dt / dx)) + +# %% Source, medium, sensor, and simulation +# -------------------- +# SOURCE +# -------------------- + +# create empty kSource +source = kSource() + +# create time varying source based on time, frequency, amplitude and phase +source_signal = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_amp, source_phase) + +# set bowl position and orientation +bowl_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0, 0] +focus_pos = [kgrid.x_vec[-1].item(), 0, 0] + +# create empty kWaveArray +karray = kWaveArray(bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate, single_precision=True) + +# add bowl shaped element to array +karray.add_annular_array(bowl_pos, source_roc, diameters, focus_pos) + +# assign binary mask to source, based on array elements and the grid +source.p_mask = karray.get_array_binary_mask(kgrid) + +# assign source signals, based on array elements, grid and signal +source.p = karray.get_distributed_source_signal(kgrid, source_signal) + +# -------------------- +# MEDIUM +# -------------------- + +# assign medium properties +medium = kWaveMedium(sound_speed=c0, density=rho0) + +# -------------------- +# SENSOR +# -------------------- + +sensor = kSensor() + +# set sensor mask to record central plane, not including the source point +sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool) +sensor.mask[(source_x_offset + 1) :, :, Nz // 2] = True + +# record the pressure +sensor.record = ["p"] + +# record only the final few periods when the field is in steady state +sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1 + +# -------------------- +# SIMULATION +# -------------------- + +# NOTE: pml_inside=False not supported in new API +sensor_data = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + pml_size="auto", + backend="cpp", + device="gpu", +) + +# extract amplitude from the sensor data +amp, _, _ = extract_amp_phase(sensor_data["p"], 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") + +# reshape data +amp = np.reshape(amp, (Nx - (source_x_offset + 1), Ny), order="F") + +# extract pressure on axis +amp_on_axis = amp[:, Ny // 2] + +# define axis vectors for plotting +x_vec = kgrid.x_vec[source_x_offset + 1 :, :] - kgrid.x_vec[source_x_offset] +y_vec = kgrid.y_vec + +# %% Analytical solution +p_axial = focused_annulus_oneil( + source_roc, np.asarray(diameters).T, source_amp / (c0 * rho0), source_phase, source_f0, c0, rho0, np.squeeze(x_vec) +) + +# %% Visualization +# plot the pressure along the focal axis of the piston +fig1, ax1 = plt.subplots(1, 1) +ax1.plot(1e3 * x_vec, 1e-6 * p_axial, "k-", label="Exact") +ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") +ax1.legend() +ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") +ax1.set_xlim(0.0, 1e3 * axial_size) +ax1.grid() + +# plot the source mask (pml is outside the grid in this example) + +# get grid weights +grid_weights = karray.get_array_grid_weights(kgrid) + +fig2, (ax2a, ax2b) = plt.subplots(1, 2) +ax2a.pcolormesh( + 1e3 * np.squeeze(kgrid.y_vec), + 1e3 * np.squeeze(kgrid.x_vec), + np.flip(source.p_mask[:, :, Nz // 2], axis=0), + shading="nearest", +) +ax2a.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") +ax2b.pcolormesh( + 1e3 * np.squeeze(kgrid.y_vec), + 1e3 * np.squeeze(kgrid.x_vec), + np.flip(grid_weights[:, :, Nz // 2], axis=0), + shading="nearest", +) +ax2b.set(xlabel="y [mm]", ylabel="x [mm]", title="Off-Grid Source Weights") +plt.tight_layout(pad=1.2) + +# plot the pressure field +fig3, ax3 = plt.subplots(1, 1) +ax3.pcolormesh(1e3 * np.squeeze(y_vec), 1e3 * np.squeeze(x_vec), np.flip(amp, axis=1), shading="gouraud") +ax3.set(xlabel="Lateral Position [mm]", ylabel="Axial Position [mm]", title="Pressure Field") +ax3.set_ylim(1e3 * x_vec[-1], 1e3 * x_vec[0]) + +plt.show() diff --git a/examples/at_focused_annular_array_3D/README.md b/examples/at_focused_annular_array_3D/README.md deleted file mode 100644 index e89c6d247..000000000 --- a/examples/at_focused_annular_array_3D/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Modelling A Focused Annular Transducer Example - -This example models a focused annular transducer in 3D. The _on-axis_ pressure is compared with an analytical solution calculated using `focused_annulus_oneil`. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading7) \ No newline at end of file diff --git a/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.ipynb b/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.ipynb deleted file mode 100644 index 7a5baff02..000000000 --- a/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.ipynb +++ /dev/null @@ -1,452 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "8b20eee6", - "metadata": {}, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5b6f8e7a-5c7d-4340-8ed4-a04099307131", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install git+https://github.com/waltsims/k-wave-python" - ] - }, - { - "cell_type": "markdown", - "id": "db1bda7e-a820-4eb4-abf9-96ab041675af", - "metadata": {}, - "source": [ - "## Modelling A Focused Annular Array Transducer In 3D Example\n", - "\n", - "This example models a focused annular array transducer in 3D. The on-axis pressure is compared with the exact solution calculated using `focused_annulus_oneil`.\n", - "\n", - "First, define the settings, import the libraries and functions needed" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "32e28e46-df00-4a08-996f-5683470c0e2b", - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "\n", - "from copy import deepcopy\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.filters import extract_amp_phase\n", - "from kwave.utils.kwave_array import kWaveArray\n", - "from kwave.utils.mapgen import focused_annulus_oneil\n", - "from kwave.utils.math import round_even\n", - "from kwave.utils.signals import create_cw_signals" - ] - }, - { - "cell_type": "markdown", - "id": "a28dbad5-6ba4-42ef-9e55-715d2383df68", - "metadata": {}, - "source": [ - "The parameters of the system are defined below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67150133-3e02-4fd9-a36d-27a9550fe84f", - "metadata": {}, - "outputs": [], - "source": [ - "# medium parameters\n", - "c0: float = 1500.0 # sound speed [m/s]\n", - "rho0: float = 1000.0 # density [kg/m^3]\n", - "\n", - "# source parameters\n", - "source_f0 = 1.0e6 # source frequency [Hz]\n", - "source_roc = 30e-3 # bowl radius of curvature [m]\n", - "source_amp = np.array([0.5e6, 1e6, 0.75e6]) # source pressure [Pa]\n", - "source_phase = np.deg2rad(np.array([0.0, 10.0, 20.0])) # source phase [radians]\n", - "\n", - "# aperture diameters of the elements given an inner, outer pairs [m]\n", - "diameters = np.array([[0.0, 5.0], [10.0, 15.0], [20.0, 25.0]]) * 1e-3\n", - "diameters = diameters.tolist()\n", - "\n", - "# grid parameters\n", - "axial_size: float = 40.0e-3 # total grid size in the axial dimension [m]\n", - "lateral_size: float = 45.0e-3 # total grid size in the lateral dimension [m]\n", - "\n", - "# computational parameters\n", - "ppw: int = 3 # number of points per wavelength\n", - "t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state)\n", - "record_periods: int = 1 # number of periods to record\n", - "cfl: float = 0.5 # CFL number\n", - "source_x_offset: int = 20 # grid points to offset the source\n", - "bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points\n", - "upsampling_rate: int = 10 # density of integration points relative to grid\n", - "verbose_level: int = 0 # verbosity of k-wave executable" - ] - }, - { - "cell_type": "markdown", - "id": "26e9fc37-096f-4e2d-af2b-32de0e085cd6", - "metadata": {}, - "source": [ - "## Grid\n", - "\n", - "Construct the grid via the `kgrid` class" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dae95498-e846-4cb1-87ac-dcc684e952bc", - "metadata": {}, - "outputs": [], - "source": [ - "# calculate the grid spacing based on the PPW and F0\n", - "dx: float = c0 / (ppw * source_f0) # [m]\n", - "\n", - "# compute the size of the grid\n", - "Nx: int = round_even(axial_size / dx) + source_x_offset\n", - "Ny: int = round_even(lateral_size / dx)\n", - "Nz: int = Ny\n", - "\n", - "grid_size_points = Vector([Nx, Ny, Nz])\n", - "grid_spacing_meters = Vector([dx, dx, dx])\n", - "\n", - "# create the k-space grid\n", - "kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)\n", - "\n", - "# compute points per period\n", - "ppp: int = round(ppw / cfl)\n", - "\n", - "# compute corresponding time spacing\n", - "dt: float = 1.0 / (ppp * source_f0)\n", - "\n", - "# create the time array using an integer number of points per period\n", - "Nt: int = int(np.round(t_end / dt))\n", - "kgrid.setTime(Nt, dt)\n", - "\n", - "# calculate the actual CFL and PPW\n", - "print('points-per-period: ' + str(c0 / (dx * source_f0)) + ' and CFL number : ' + str(c0 * dt / dx))" - ] - }, - { - "cell_type": "markdown", - "id": "b39b121c-cc4d-45ee-9d0a-3cee00c690f5", - "metadata": {}, - "source": [ - "## Source\n", - "\n", - "Define the source, using the `kWaveArray` class and the `add_bowl_element` method along with a continuous wave signal." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d8474039-e3b7-4f09-b76d-6bfaf536bc21", - "metadata": {}, - "outputs": [], - "source": [ - "source = kSource()\n", - "\n", - "# create time varying source\n", - "source_signal = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_amp, source_phase)\n", - "\n", - "# create empty kWaveArray\n", - "karray = kWaveArray(bli_tolerance=bli_tolerance,\n", - " upsampling_rate=upsampling_rate,\n", - " single_precision=True)\n", - "\n", - "# set bowl position and orientation\n", - "bowl_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0, 0]\n", - "focus_pos = [kgrid.x_vec[-1].item(), 0, 0]\n", - "\n", - "# add bowl shaped element to array\n", - "karray.add_annular_array(bowl_pos, source_roc, diameters, focus_pos)\n", - "\n", - "# assign binary mask\n", - "source.p_mask = karray.get_array_binary_mask(kgrid)\n", - "\n", - "# assign source signals\n", - "source.p = karray.get_distributed_source_signal(kgrid, source_signal)" - ] - }, - { - "cell_type": "markdown", - "id": "d9e68ba8-f59f-441d-a86f-bd649571a752", - "metadata": {}, - "source": [ - "## Medium\n", - "\n", - "The medium is water. Neither nonlinearity nor attenuation are considered." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f402c35a-43db-49cc-8947-afd1842f5c63", - "metadata": {}, - "outputs": [], - "source": [ - "# assign medium properties\n", - "medium = kWaveMedium(sound_speed=c0, density=rho0)" - ] - }, - { - "cell_type": "markdown", - "id": "acb68ad1-1f98-4dd5-b000-301386aab42e", - "metadata": {}, - "source": [ - "## Sensor\n", - "\n", - "The sensor class defines what acoustic information is recorded." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "882f547c-9b94-4da5-8157-3157b983bdf3", - "metadata": {}, - "outputs": [], - "source": [ - "sensor = kSensor()\n", - "\n", - "# set sensor mask to record central plane, not including the source point\n", - "sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool)\n", - "sensor.mask[(source_x_offset + 1):, :, Nz // 2] = True\n", - "\n", - "# record the pressure\n", - "sensor.record = ['p']\n", - "\n", - "# record only the final few periods when the field is in steady state\n", - "sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1" - ] - }, - { - "cell_type": "markdown", - "id": "7df50ac4-3e8e-4cdf-a275-90b54c7e5bea", - "metadata": {}, - "source": [ - "## Simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "66826f6c-b520-463b-a51f-aa4c15ed72a5", - "metadata": {}, - "outputs": [], - "source": [ - "simulation_options = SimulationOptions(pml_auto=True,\n", - " data_recast=True,\n", - " save_to_disk=True,\n", - " save_to_disk_exit=False,\n", - " pml_inside=False)\n", - "\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True,\n", - " delete_data=False,\n", - " verbose_level=0)\n", - "\n", - "sensor_data = kspaceFirstOrder3D(medium=deepcopy(medium),\n", - " kgrid=deepcopy(kgrid),\n", - " source=deepcopy(source),\n", - " sensor=deepcopy(sensor),\n", - " simulation_options=simulation_options,\n", - " execution_options=execution_options)" - ] - }, - { - "cell_type": "markdown", - "id": "77173d6d-bacc-46ee-be2b-e085d4985401", - "metadata": {}, - "source": [ - "## Post-processing\n", - "\n", - "Extract amplitude from the sensor data, using the Fourier transform. The data can be reshaped to match the spatial extents of the domain. The on-axis pressure amplitudes found and axes for plotting defined." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fbd76b31-58b4-4310-bdc6-0b7c6e7818e9", - "metadata": {}, - "outputs": [], - "source": [ - "# extract amplitude from the sensor data\n", - "amp, _, _ = extract_amp_phase(sensor_data['p'].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window='Rectangular')\n", - "\n", - "# reshape data\n", - "amp = np.reshape(amp, (Nx - (source_x_offset + 1), Ny), order='F')\n", - "\n", - "# extract pressure on axis\n", - "amp_on_axis = amp[:, Ny // 2]\n", - "\n", - "# define axis vectors for plotting\n", - "x_vec = np.squeeze(kgrid.x_vec[(source_x_offset + 1):, :] - kgrid.x_vec[source_x_offset])\n", - "y_vec = kgrid.y_vec" - ] - }, - { - "cell_type": "markdown", - "id": "11ec4eed-1349-4ea3-a2e9-61ede65f3702", - "metadata": {}, - "source": [ - "## Analytical Solution\n", - "\n", - "An analytical expression cam be found in Pierce[[1]](#cite_note-1). Given a transdcuer of radius $a$, wavenumber $k= 2 \\pi f / c$, where $f$ is the frequency, speed of sound $c$, and a unit normal vector to transducer surface, $\\hat{v}_n$, the on-axis pressure is given by\n", - "\n", - "$$\n", - "p_{\\mathrm{ref}}(z) = −2 \\, i \\, \\rho \\, c \\, \\hat{v}_n \\, e^{i k \\left( z + \\sqrt{z^2 + a^2} \\right) \\big/ 2} \\sin \\left( \\dfrac{k}{2} \\left( \\sqrt{z^2 + a^2} − z \\right) \\right).\n", - "$$\n", - "\n", - "[[1]](#cite_ref-1) A. D. Pierce, _\"Acoustics: An Introduction to its Physical Principles and Applications\"_ Springer (2019)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d5691bd3-295d-49c1-b815-13f33bc9b952", - "metadata": {}, - "outputs": [], - "source": [ - "p_axial = focused_annulus_oneil(source_roc, np.asarray(diameters).T, source_amp / (c0 * rho0), source_phase, source_f0, c0, rho0, np.squeeze(x_vec))" - ] - }, - { - "cell_type": "markdown", - "id": "373af282-a91a-42c2-82dd-7678de72b1fd", - "metadata": {}, - "source": [ - "## Visualisation\n", - "\n", - "First plot the pressure along the focal axis of the piston" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e38cbfea-115d-4196-b3cc-2585a56e861f", - "metadata": {}, - "outputs": [], - "source": [ - "fig1, ax1 = plt.subplots(1, 1)\n", - "ax1.plot(1e3 * x_vec, 1e-6 * p_axial, 'k-', label='Analytical')\n", - "ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, 'b.', label='k-Wave')\n", - "ax1.legend()\n", - "ax1.set(xlabel='Axial Position [mm]',\n", - " ylabel='Pressure [MPa]',\n", - " title='Axial Pressure')\n", - "ax1.set_xlim(0.0, 1e3 * axial_size)\n", - "ax1.set_ylim(0.0, 6)\n", - "ax1.grid()" - ] - }, - { - "cell_type": "markdown", - "id": "03150718-35bc-4796-990c-7469b6177159", - "metadata": {}, - "source": [ - "Next plot the source mask (pml is outside the grid in this example). This means getting the grid weights first" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f04f9d92-da57-4627-969d-3c8d96f4c396", - "metadata": {}, - "outputs": [], - "source": [ - "# get grid weights\n", - "grid_weights = karray.get_array_grid_weights(kgrid)\n", - "\n", - "fig2, (ax2a, ax2b) = plt.subplots(1, 2)\n", - "ax2a.pcolormesh(1e3 * np.squeeze(kgrid.y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec),\n", - " np.flip(source.p_mask[:, :, int(np.ceil(Nz / 2))], axis=0),\n", - " shading='nearest')\n", - "ax2a.set(xlabel='y [mm]',\n", - " ylabel='x [mm]',\n", - " title='Source Mask')\n", - "ax2b.pcolormesh(1e3 * np.squeeze(kgrid.y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec),\n", - " np.flip(grid_weights[:, :, int(np.ceil(Nz / 2))], axis=0),\n", - " shading='nearest')\n", - "ax2b.set_xlabel('y [mm]')\n", - "ax2b.set_ylabel('x [mm]')\n", - "_ = ax2b.set_title('Off-Grid Source Weights')\n", - "plt.tight_layout(pad=1.2)" - ] - }, - { - "cell_type": "markdown", - "id": "d51ef784-06c3-4c5f-a478-e6864aa12041", - "metadata": {}, - "source": [ - "Finally, plot the pressure field" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "109c084a-29fb-4180-88e7-e37ecf3b3927", - "metadata": {}, - "outputs": [], - "source": [ - "fig3, ax3 = plt.subplots(1, 1)\n", - "p3 = ax3.pcolormesh(1e3 * np.squeeze(y_vec),\n", - " 1e3 * np.squeeze(x_vec),\n", - " np.flip(amp, axis=1) / 1e6,\n", - " shading='gouraud')\n", - "ax3.set(xlabel='Lateral Position [mm]',\n", - " ylabel='Axial Position [mm]',\n", - " title='Pressure Field')\n", - "ax3.set_ylim(1e3 * x_vec[-1], 1e3 * x_vec[0])\n", - "cbar3 = fig3.colorbar(p3, ax=ax3)\n", - "_ = cbar3.ax.set_title('[MPa]', fontsize='small')" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "include_colab_link": true, - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/at_focused_bowl_3D/at_focused_bowl_3D.py b/examples/at_focused_bowl_3D.py similarity index 77% rename from examples/at_focused_bowl_3D/at_focused_bowl_3D.py rename to examples/at_focused_bowl_3D.py index 83909d7ed..2afd36a1d 100644 --- a/examples/at_focused_bowl_3D/at_focused_bowl_3D.py +++ b/examples/at_focused_bowl_3D.py @@ -1,233 +1,216 @@ -from copy import deepcopy - -import matplotlib.pyplot as plt -import numpy as np - -from kwave.data import Vector -from kwave.kgrid import kWaveGrid -from kwave.kmedium import kWaveMedium -from kwave.ksensor import kSensor -from kwave.ksource import kSource -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions -from kwave.utils.filters import extract_amp_phase -from kwave.utils.kwave_array import kWaveArray -from kwave.utils.mapgen import focused_bowl_oneil -from kwave.utils.math import round_even -from kwave.utils.signals import create_cw_signals - -# Modelling A Focused Bowl Transducer In 3D Example - -# This example models a focused bowl transducer in 3D. The on-axis pressure -# is compared with the exact solution calculated using focused_bowl_oneil. - -verbose: bool = False - -# medium parameters -c0: float = 1500.0 # sound speed [m/s] -rho0: float = 1000.0 # density [kg/m^3] - -# source parameters -source_f0 = 1.0e6 # source frequency [Hz] -source_roc = 30e-3 # bowl radius of curvature [m] -source_diameter = 30e-3 # bowl aperture diameter [m] -source_amp = 1.0e6 # source pressure [Pa] -source_phase = 0.0 # source phase [radians] - -# grid parameters -axial_size: float = 50.0e-3 # total grid size in the axial dimension [m] -lateral_size: float = 40.0e-3 # total grid size in the lateral dimension [m] - -# computational parameters -ppw: int = 3 # number of points per wavelength -t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state) -record_periods: int = 1 # number of periods to record -cfl: float = 0.5 # CFL number -source_x_offset: int = 20 # grid points to offset the source -bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points -upsampling_rate: int = 10 # density of integration points relative to grid - -# ========================================================================= -# RUN SIMULATION -# ========================================================================= - -# -------------------- -# GRID -# -------------------- - -# calculate the grid spacing based on the PPW and F0 -dx: float = c0 / (ppw * source_f0) # [m] - -# compute the size of the grid -Nx: int = round_even(axial_size / dx) + source_x_offset -Ny: int = round_even(lateral_size / dx) -Nz: int = Ny - -grid_size_points = Vector([Nx, Ny, Nz]) -grid_spacing_meters = Vector([dx, dx, dx]) - -# create the k-space grid -kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) - -# compute points per temporal period -ppp: int = round(ppw / cfl) - -# compute corresponding time spacing -dt: float = 1.0 / (ppp * source_f0) - -# create the time array using an integer number of points per period -Nt: int = int(np.round(t_end / dt)) -kgrid.setTime(Nt, dt) - -# calculate the actual CFL and PPW -if verbose: - print("PPW = " + str(c0 / (dx * source_f0))) - print("CFL = " + str(c0 * dt / dx)) - -# -------------------- -# SOURCE -# -------------------- - -source = kSource() - -# create time varying source -source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, np.array([source_amp]), np.array([source_phase])) - -# set bowl position and orientation -bowl_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0.0, 0.0] -focus_pos = [kgrid.x_vec[-1].item(), 0.0, 0.0] - -# create empty kWaveArray -karray = kWaveArray(bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate, single_precision=True) - -# add bowl shaped element -karray.add_bowl_element(bowl_pos, source_roc, source_diameter, focus_pos) - -# assign binary mask -source.p_mask = karray.get_array_binary_mask(kgrid) - -# assign source signals -source.p = karray.get_distributed_source_signal(kgrid, source_sig) - -# -------------------- -# MEDIUM -# -------------------- - -# assign medium properties -medium = kWaveMedium(sound_speed=c0, density=rho0) - -# -------------------- -# SENSOR -# -------------------- - -sensor = kSensor() - -# set sensor mask to record central plane, not including the source point -sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool) -sensor.mask[(source_x_offset + 1) : -1, :, Nz // 2] = True - -# record the pressure -sensor.record = ["p"] - -# record only the final few periods when the field is in steady state -sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1 - - -# -------------------- -# SIMULATION -# -------------------- - -simulation_options = SimulationOptions(pml_auto=True, data_recast=True, save_to_disk=True, save_to_disk_exit=False, pml_inside=False) - -execution_options = SimulationExecutionOptions(is_gpu_simulation=True, delete_data=False, verbose_level=2) - -sensor_data = kspaceFirstOrder3D( - medium=deepcopy(medium), - kgrid=deepcopy(kgrid), - source=deepcopy(source), - sensor=deepcopy(sensor), - simulation_options=simulation_options, - execution_options=execution_options, -) - -# extract amplitude from the sensor data -amp, _, _ = extract_amp_phase(sensor_data["p"].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") - -# reshape data -amp = np.reshape(amp, (Nx - (source_x_offset + 2), Ny), order="F") - -# extract pressure on axis -amp_on_axis = amp[:, Ny // 2] - -# define axis vectors for plotting -x_vec = kgrid.x_vec[(source_x_offset + 1) : -1, :] - kgrid.x_vec[source_x_offset] -y_vec = kgrid.y_vec - -# ========================================================================= -# ANALYTICAL SOLUTION -# ========================================================================= - -# calculate the wavenumber -knumber = 2.0 * np.pi * source_f0 / c0 - -# define axis -x_max = Nx * dx -delta_x = x_max / 10000.0 -x_ref = np.arange(0.0, x_max + delta_x, delta_x) - -Z = source_amp / (c0 * rho0) - -# calculate analytical solution -p_ref_axial, _, _ = focused_bowl_oneil(source_roc, source_diameter, Z, source_f0, c0, rho0, axial_positions=x_ref) - -# calculate analytical solution at exactly the same points as the simulation -p_ref_axial_kw, _, _ = focused_bowl_oneil(source_roc, source_diameter, Z, source_f0, c0, rho0, axial_positions=np.squeeze(x_vec)) - -L2_error = 100 * np.linalg.norm(p_ref_axial_kw - amp_on_axis, ord=2) -Linf_error = 100 * np.linalg.norm(p_ref_axial_kw - amp_on_axis, ord=np.inf) - -# ========================================================================= -# VISUALISATION -# ========================================================================= - -# plot the pressure along the focal axis of the piston -fig1, ax1 = plt.subplots(1, 1) -ax1.plot(1e3 * x_ref, 1e-6 * p_ref_axial, "k-", label="Exact") -ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") -ax1.legend() -ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") -ax1.set_xlim(0.0, 1e3 * axial_size) -ax1.set_ylim(0.0, 20) -ax1.grid() - -# plot the source mask (pml is outside the grid in this example) - -# get grid weights -grid_weights = karray.get_array_grid_weights(kgrid) - -fig2, (ax2a, ax2b) = plt.subplots(1, 2) -ax2a.pcolormesh( - 1e3 * np.squeeze(kgrid.y_vec), - 1e3 * np.squeeze(kgrid.x_vec), - np.flip(source.p_mask[:, :, Nz // 2], axis=0), - shading="nearest", -) -ax2a.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") -ax2b.pcolormesh( - 1e3 * np.squeeze(kgrid.y_vec), - 1e3 * np.squeeze(kgrid.x_vec), - np.flip(grid_weights[:, :, Nz // 2], axis=0), - shading="nearest", -) -ax2b.set(xlabel="y [mm]", ylabel="x [mm]", title="Off-Grid Source Weights") -plt.tight_layout(pad=1.2) - -# plot the pressure field -fig3, ax3 = plt.subplots(1, 1) -ax3.pcolormesh(1e3 * np.squeeze(y_vec), 1e3 * np.squeeze(x_vec), np.flip(amp, axis=1), shading="gouraud") -ax3.set(xlabel="Lateral Position [mm]", ylabel="Axial Position [mm]", title="Pressure Field") -ax3.set_ylim(1e3 * x_vec[-1], 1e3 * x_vec[0]) - -# show figures -plt.show() +# %% [markdown] +# # Focused Bowl Transducer in 3D +# Modelling a focused bowl transducer and comparing on-axis pressure with O'Neil analytical solution. + +# %% +import matplotlib.pyplot as plt +import numpy as np + +from kwave.data import Vector +from kwave.kgrid import kWaveGrid +from kwave.kmedium import kWaveMedium +from kwave.ksensor import kSensor +from kwave.ksource import kSource +from kwave.kspaceFirstOrder import kspaceFirstOrder +from kwave.utils.filters import extract_amp_phase +from kwave.utils.kwave_array import kWaveArray +from kwave.utils.mapgen import focused_bowl_oneil +from kwave.utils.math import round_even +from kwave.utils.signals import create_cw_signals + +# %% Parameters +verbose: bool = False + +# medium parameters +c0: float = 1500.0 # sound speed [m/s] +rho0: float = 1000.0 # density [kg/m^3] + +# source parameters +source_f0 = 1.0e6 # source frequency [Hz] +source_roc = 30e-3 # bowl radius of curvature [m] +source_diameter = 30e-3 # bowl aperture diameter [m] +source_amp = 1.0e6 # source pressure [Pa] +source_phase = 0.0 # source phase [radians] + +# grid parameters +axial_size: float = 50.0e-3 # total grid size in the axial dimension [m] +lateral_size: float = 40.0e-3 # total grid size in the lateral dimension [m] + +# computational parameters +ppw: int = 3 # number of points per wavelength +t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state) +record_periods: int = 1 # number of periods to record +cfl: float = 0.5 # CFL number +source_x_offset: int = 20 # grid points to offset the source +bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points +upsampling_rate: int = 10 # density of integration points relative to grid + +# %% Grid setup +# calculate the grid spacing based on the PPW and F0 +dx: float = c0 / (ppw * source_f0) # [m] + +# compute the size of the grid +Nx: int = round_even(axial_size / dx) + source_x_offset +Ny: int = round_even(lateral_size / dx) +Nz: int = Ny + +grid_size_points = Vector([Nx, Ny, Nz]) +grid_spacing_meters = Vector([dx, dx, dx]) + +# create the k-space grid +kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) + +# compute points per temporal period +ppp: int = round(ppw / cfl) + +# compute corresponding time spacing +dt: float = 1.0 / (ppp * source_f0) + +# create the time array using an integer number of points per period +Nt: int = int(np.round(t_end / dt)) +kgrid.setTime(Nt, dt) + +# calculate the actual CFL and PPW +if verbose: + print("PPW = " + str(c0 / (dx * source_f0))) + print("CFL = " + str(c0 * dt / dx)) + +# %% Source, medium, sensor, and simulation +# -------------------- +# SOURCE +# -------------------- + +source = kSource() + +# create time varying source +source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, np.array([source_amp]), np.array([source_phase])) + +# set bowl position and orientation +bowl_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0.0, 0.0] +focus_pos = [kgrid.x_vec[-1].item(), 0.0, 0.0] + +# create empty kWaveArray +karray = kWaveArray(bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate, single_precision=True) + +# add bowl shaped element +karray.add_bowl_element(bowl_pos, source_roc, source_diameter, focus_pos) + +# assign binary mask +source.p_mask = karray.get_array_binary_mask(kgrid) + +# assign source signals +source.p = karray.get_distributed_source_signal(kgrid, source_sig) + +# -------------------- +# MEDIUM +# -------------------- + +# assign medium properties +medium = kWaveMedium(sound_speed=c0, density=rho0) + +# -------------------- +# SENSOR +# -------------------- + +sensor = kSensor() + +# set sensor mask to record central plane, not including the source point +sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool) +sensor.mask[(source_x_offset + 1) : -1, :, Nz // 2] = True + +# record the pressure +sensor.record = ["p"] + +# record only the final few periods when the field is in steady state +sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1 + + +# -------------------- +# SIMULATION +# -------------------- + +# NOTE: pml_inside=False not supported in new API +sensor_data = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + pml_size="auto", + backend="cpp", + device="gpu", +) + +# extract amplitude from the sensor data +amp, _, _ = extract_amp_phase(sensor_data["p"], 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") + +# reshape data +amp = np.reshape(amp, (Nx - (source_x_offset + 2), Ny), order="F") + +# extract pressure on axis +amp_on_axis = amp[:, Ny // 2] + +# define axis vectors for plotting +x_vec = kgrid.x_vec[(source_x_offset + 1) : -1, :] - kgrid.x_vec[source_x_offset] +y_vec = kgrid.y_vec + +# %% Analytical solution +# calculate the wavenumber +knumber = 2.0 * np.pi * source_f0 / c0 + +# define axis +x_max = Nx * dx +delta_x = x_max / 10000.0 +x_ref = np.arange(0.0, x_max + delta_x, delta_x) + +Z = source_amp / (c0 * rho0) + +# calculate analytical solution +p_ref_axial, _, _ = focused_bowl_oneil(source_roc, source_diameter, Z, source_f0, c0, rho0, axial_positions=x_ref) + +# calculate analytical solution at exactly the same points as the simulation +p_ref_axial_kw, _, _ = focused_bowl_oneil(source_roc, source_diameter, Z, source_f0, c0, rho0, axial_positions=np.squeeze(x_vec)) + +L2_error = 100 * np.linalg.norm(p_ref_axial_kw - amp_on_axis, ord=2) +Linf_error = 100 * np.linalg.norm(p_ref_axial_kw - amp_on_axis, ord=np.inf) + +# %% Visualization +# plot the pressure along the focal axis of the piston +fig1, ax1 = plt.subplots(1, 1) +ax1.plot(1e3 * x_ref, 1e-6 * p_ref_axial, "k-", label="Exact") +ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") +ax1.legend() +ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") +ax1.set_xlim(0.0, 1e3 * axial_size) +ax1.set_ylim(0.0, 20) +ax1.grid() + +# plot the source mask (pml is outside the grid in this example) + +# get grid weights +grid_weights = karray.get_array_grid_weights(kgrid) + +fig2, (ax2a, ax2b) = plt.subplots(1, 2) +ax2a.pcolormesh( + 1e3 * np.squeeze(kgrid.y_vec), + 1e3 * np.squeeze(kgrid.x_vec), + np.flip(source.p_mask[:, :, Nz // 2], axis=0), + shading="nearest", +) +ax2a.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") +ax2b.pcolormesh( + 1e3 * np.squeeze(kgrid.y_vec), + 1e3 * np.squeeze(kgrid.x_vec), + np.flip(grid_weights[:, :, Nz // 2], axis=0), + shading="nearest", +) +ax2b.set(xlabel="y [mm]", ylabel="x [mm]", title="Off-Grid Source Weights") +plt.tight_layout(pad=1.2) + +# plot the pressure field +fig3, ax3 = plt.subplots(1, 1) +ax3.pcolormesh(1e3 * np.squeeze(y_vec), 1e3 * np.squeeze(x_vec), np.flip(amp, axis=1), shading="gouraud") +ax3.set(xlabel="Lateral Position [mm]", ylabel="Axial Position [mm]", title="Pressure Field") +ax3.set_ylim(1e3 * x_vec[-1], 1e3 * x_vec[0]) + +# show figures +plt.show() diff --git a/examples/at_focused_bowl_3D/README.md b/examples/at_focused_bowl_3D/README.md deleted file mode 100644 index f40f95a29..000000000 --- a/examples/at_focused_bowl_3D/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Modelling A Focused Bowl Transducer Example - -This example models a focused bowl transducer in 3D. The _on-axis_ pressure is compared with an analytical solution calculated using `focused_bowl_oneil`. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading5) \ No newline at end of file diff --git a/examples/at_focused_bowl_3D/at_focused_bowl_3D.ipynb b/examples/at_focused_bowl_3D/at_focused_bowl_3D.ipynb deleted file mode 100644 index 74ddf92f5..000000000 --- a/examples/at_focused_bowl_3D/at_focused_bowl_3D.ipynb +++ /dev/null @@ -1,451 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "5b6f8e7a-5c7d-4340-8ed4-a04099307131", - "metadata": {}, - "outputs": [], - "source": [ - "%pip install git+https://github.com/waltsims/k-wave-python " - ] - }, - { - "cell_type": "markdown", - "id": "db1bda7e-a820-4eb4-abf9-96ab041675af", - "metadata": {}, - "source": [ - "## Modelling A Focused Bowl Transducer In 3D Example\n", - "\n", - "This example models a focused bowl transducer in 3D. The on-axis pressure is compared with the exact solution calculated using `focused_bowl_oneil` function.\n", - "\n", - "First, define the settings, import the libraries and functions needed:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "32e28e46-df00-4a08-996f-5683470c0e2b", - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "\n", - "from copy import deepcopy\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.filters import extract_amp_phase\n", - "from kwave.utils.kwave_array import kWaveArray\n", - "from kwave.utils.mapgen import focused_bowl_oneil\n", - "from kwave.utils.math import round_even\n", - "from kwave.utils.signals import create_cw_signals\n" - ] - }, - { - "cell_type": "markdown", - "id": "a28dbad5-6ba4-42ef-9e55-715d2383df68", - "metadata": {}, - "source": [ - "The parameters of the system are defined below" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67150133-3e02-4fd9-a36d-27a9550fe84f", - "metadata": {}, - "outputs": [], - "source": [ - "# medium parameters\n", - "c0: float = 1500.0 # sound speed [m/s]\n", - "rho0: float = 1000.0 # density [kg/m^3]\n", - "\n", - "# source parameters\n", - "source_f0 = 1.0e6 # source frequency [Hz]\n", - "source_roc = 30e-3 # bowl radius of curvature [m]\n", - "source_diameter = 30e-3 # bowl aperture diameter [m]\n", - "source_amp = 1.0e6 # source pressure [Pa]\n", - "source_phase = np.array([0.0]) # source phase [radians]\n", - "\n", - "# grid parameters\n", - "axial_size: float = 50.0e-3 # total grid size in the axial dimension [m]\n", - "lateral_size: float = 40.0e-3 # total grid size in the lateral dimension [m]\n", - "\n", - "# computational parameters\n", - "ppw: int = 3 # number of points per wavelength\n", - "t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state)\n", - "record_periods: int = 1 # number of periods to record\n", - "cfl: float = 0.5 # CFL number\n", - "source_x_offset: int = 20 # grid points to offset the source\n", - "bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points\n", - "upsampling_rate: int = 10 # density of integration points relative to grid\n", - "verbose_level: int = 0 # verbosity of k-wave executable" - ] - }, - { - "cell_type": "markdown", - "id": "26e9fc37-096f-4e2d-af2b-32de0e085cd6", - "metadata": {}, - "source": [ - "## Grid\n", - "\n", - "Construct the grid via the `kgrid` class" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dae95498-e846-4cb1-87ac-dcc684e952bc", - "metadata": {}, - "outputs": [], - "source": [ - "# calculate the grid spacing based on the points-per-wavelength and fundamental frequency\n", - "dx: float = c0 / (ppw * source_f0) # [m]\n", - "\n", - "# compute the size of the grid\n", - "Nx: int = round_even(axial_size / dx) + source_x_offset\n", - "Ny: int = round_even(lateral_size / dx)\n", - "Nz: int = Ny\n", - "\n", - "grid_size_points = Vector([Nx, Ny, Nz])\n", - "grid_spacing_meters = Vector([dx, dx, dx])\n", - "\n", - "# create the k-space grid\n", - "kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)\n", - "\n", - "# compute points per period\n", - "ppp: int = round(ppw / cfl)\n", - "\n", - "# compute corresponding time spacing\n", - "dt: float = 1.0 / (ppp * source_f0)\n", - "\n", - "# create the time array using an integer number of points per period\n", - "Nt: int = int(np.round(t_end / dt))\n", - "kgrid.setTime(Nt, dt)\n", - "\n", - "# calculate the actual CFL and PPW\n", - "print('points-per-period: ' + str(c0 / (dx * source_f0)) + ' and CFL number : ' + str(c0 * dt / dx))" - ] - }, - { - "cell_type": "markdown", - "id": "b39b121c-cc4d-45ee-9d0a-3cee00c690f5", - "metadata": {}, - "source": [ - "## Source\n", - "\n", - "Define the source, using the `kWaveArray` class and the `add_bowl_element` method along with a continuous wave signal." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d8474039-e3b7-4f09-b76d-6bfaf536bc21", - "metadata": {}, - "outputs": [], - "source": [ - "source = kSource()\n", - "\n", - "# create time varying source\n", - "source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, np.array([source_amp]), source_phase)\n", - "\n", - "# set bowl position and orientation\n", - "bowl_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0.0, 0.0]\n", - "focus_pos = [kgrid.x_vec[-1].item(), 0.0, 0.0]\n", - "\n", - "# create empty kWaveArray\n", - "karray = kWaveArray(bli_tolerance=bli_tolerance,\n", - " upsampling_rate=upsampling_rate,\n", - " single_precision=True)\n", - "\n", - "# add bowl shaped element\n", - "karray.add_bowl_element(bowl_pos, source_roc, source_diameter, focus_pos)\n", - "\n", - "# assign binary mask\n", - "source.p_mask = karray.get_array_binary_mask(kgrid)\n", - "\n", - "# assign source signals\n", - "source.p = karray.get_distributed_source_signal(kgrid, source_sig)" - ] - }, - { - "cell_type": "markdown", - "id": "d9e68ba8-f59f-441d-a86f-bd649571a752", - "metadata": {}, - "source": [ - "## Medium\n", - "\n", - "The medium is water. Neither nonlinearity nor attenuation are considered." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f402c35a-43db-49cc-8947-afd1842f5c63", - "metadata": {}, - "outputs": [], - "source": [ - "# assign medium properties\n", - "medium = kWaveMedium(sound_speed=c0, density=rho0)" - ] - }, - { - "cell_type": "markdown", - "id": "acb68ad1-1f98-4dd5-b000-301386aab42e", - "metadata": {}, - "source": [ - "## Sensor\n", - "\n", - "The sensor class defines what acoustic information is recorded.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "882f547c-9b94-4da5-8157-3157b983bdf3", - "metadata": {}, - "outputs": [], - "source": [ - "sensor = kSensor()\n", - "\n", - "# set sensor mask to record central plane, not including the source point\n", - "sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool)\n", - "sensor.mask[(source_x_offset + 1):-1, :, Nz // 2] = True\n", - "\n", - "# record the pressure\n", - "sensor.record = ['p']\n", - "\n", - "# record only the final few periods when the field is in steady state\n", - "sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1" - ] - }, - { - "cell_type": "markdown", - "id": "7df50ac4-3e8e-4cdf-a275-90b54c7e5bea", - "metadata": {}, - "source": [ - "## Simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "66826f6c-b520-463b-a51f-aa4c15ed72a5", - "metadata": {}, - "outputs": [], - "source": [ - "simulation_options = SimulationOptions(pml_auto=True,\n", - " data_recast=True,\n", - " save_to_disk=True,\n", - " save_to_disk_exit=False,\n", - " pml_inside=False)\n", - "\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True,\n", - " delete_data=False,\n", - " verbose_level=0)\n", - "\n", - "sensor_data = kspaceFirstOrder3D(medium=deepcopy(medium),\n", - " kgrid=deepcopy(kgrid),\n", - " source=deepcopy(source),\n", - " sensor=deepcopy(sensor),\n", - " simulation_options=simulation_options,\n", - " execution_options=execution_options)" - ] - }, - { - "cell_type": "markdown", - "id": "77173d6d-bacc-46ee-be2b-e085d4985401", - "metadata": {}, - "source": [ - "## Post-processing\n", - "\n", - "Extract amplitude from the sensor data, using the Fourier transform. The data can be reshaped to match the spatial extents of the domain. The on-axis pressure amplitudes found and axes for plotting defined." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fbd76b31-58b4-4310-bdc6-0b7c6e7818e9", - "metadata": {}, - "outputs": [], - "source": [ - "# extract amplitude from the sensor data\n", - "amp, _, _ = extract_amp_phase(sensor_data['p'].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window='Rectangular')\n", - "\n", - "# reshape data\n", - "amp = np.reshape(amp, (Nx - (source_x_offset+2), Ny), order='F')\n", - "\n", - "# extract pressure on axis\n", - "amp_on_axis = amp[:, Ny // 2]\n", - "\n", - "# define axis vectors for plotting\n", - "x_vec = kgrid.x_vec[(source_x_offset + 1):-1, :] - kgrid.x_vec[source_x_offset]\n", - "y_vec = kgrid.y_vec" - ] - }, - { - "cell_type": "markdown", - "id": "11ec4eed-1349-4ea3-a2e9-61ede65f3702", - "metadata": {}, - "source": [ - "## Analytical Solution\n", - "\n", - "An analytical expression cam be found in Pierce[[1]](#cite_note-1). Given a transdcuer of radius $a$, wavenumber $k= 2 \\pi f / c$, where $f$ is the frequency, speed of sound $c$, density $\\rho$, and a unit normal vector to transducer surface, $\\hat{v}_n$, the on-axis pressure, along $z$, is given by\n", - "\n", - "$$\n", - "p_{\\mathrm{ref}}(z) = −2 \\, i \\, \\rho \\, c \\, \\hat{v}_n \\, e^{i k \\left( z + \\sqrt{z^2 + a^2} \\right) \\big/ 2} \\sin \\left( \\dfrac{k}{2} \\left( \\sqrt{z^2 + a^2} − z \\right) \\right).\n", - "$$\n", - "\n", - "[[1]](#cite_ref-1) A. D. Pierce, _\"Acoustics: An Introduction to its Physical Principles and Applications\"_ Springer (2019)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d5691bd3-295d-49c1-b815-13f33bc9b952", - "metadata": {}, - "outputs": [], - "source": [ - "# define axis\n", - "x_max = Nx * dx\n", - "delta_x = x_max / 10000.0\n", - "x_ref = np.arange(0.0, x_max + delta_x, delta_x)\n", - "\n", - "Z = source_amp / (c0 * rho0)\n", - "\n", - "# calculate analytical solution\n", - "p_ref_axial, _, _ = focused_bowl_oneil(source_roc, source_diameter, Z, source_f0, c0, rho0, axial_positions=x_ref)\n", - "\n", - "# calculate analytical solution at exactly the same points as the simulation\n", - "p_ref_axial_kw, _, _ = focused_bowl_oneil(source_roc, source_diameter, Z, source_f0, c0, rho0, axial_positions=np.squeeze(x_vec))" - ] - }, - { - "cell_type": "markdown", - "id": "373af282-a91a-42c2-82dd-7678de72b1fd", - "metadata": {}, - "source": [ - "## Visualisation\n", - "\n", - "First plot the pressure along the focal axis of the piston" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e38cbfea-115d-4196-b3cc-2585a56e861f", - "metadata": {}, - "outputs": [], - "source": [ - "fig1, ax1 = plt.subplots(1, 1)\n", - "ax1.plot(1e3 * x_ref, 1e-6 * p_ref_axial, 'k-', label='Analytical')\n", - "ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, 'b.', label='k-Wave')\n", - "ax1.legend()\n", - "ax1.set(xlabel='Axial Position [mm]',\n", - " ylabel='Pressure [MPa]',\n", - " title='Axial Pressure')\n", - "ax1.set_xlim(0.0, 1e3 * axial_size)\n", - "ax1.set_ylim(0.0, 20)\n", - "ax1.grid()" - ] - }, - { - "cell_type": "markdown", - "id": "03150718-35bc-4796-990c-7469b6177159", - "metadata": {}, - "source": [ - "Next plot the source mask (pml is outside the grid in this example). This means getting the grid weights first" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f04f9d92-da57-4627-969d-3c8d96f4c396", - "metadata": {}, - "outputs": [], - "source": [ - "# get grid weights\n", - "grid_weights = karray.get_array_grid_weights(kgrid)\n", - "\n", - "fig2, (ax2a, ax2b) = plt.subplots(1, 2)\n", - "ax2a.pcolormesh(1e3 * np.squeeze(kgrid.y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec),\n", - " np.flip(source.p_mask[:, :, Nz // 2], axis=0),\n", - " shading='nearest')\n", - "ax2a.set(xlabel='y [mm]',\n", - " ylabel='x [mm]',\n", - " title='Source Mask')\n", - "ax2b.pcolormesh(1e3 * np.squeeze(kgrid.y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec),\n", - " np.flip(grid_weights[:, :, Nz // 2], axis=0),\n", - " shading='nearest')\n", - "ax2b.set_xlabel('y [mm]')\n", - "ax2b.set_ylabel('x [mm]')\n", - "_ = ax2b.set_title('Off-Grid Source Weights')\n", - "plt.tight_layout(pad=1.2)" - ] - }, - { - "cell_type": "markdown", - "id": "d51ef784-06c3-4c5f-a478-e6864aa12041", - "metadata": {}, - "source": [ - "Finally, plot the pressure field" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "109c084a-29fb-4180-88e7-e37ecf3b3927", - "metadata": {}, - "outputs": [], - "source": [ - "fig3, ax3 = plt.subplots(1, 1)\n", - "p3 = ax3.pcolormesh(1e3 * np.squeeze(y_vec),\n", - " 1e3 * np.squeeze(x_vec),\n", - " np.flip(amp, axis=1) / 1e6,\n", - " shading='gouraud')\n", - "ax3.set(xlabel='Lateral Position [mm]',\n", - " ylabel='Axial Position [mm]',\n", - " title='Pressure Field')\n", - "ax3.set_ylim(1e3 * x_vec[-1], 1e3 * x_vec[0])\n", - "cbar3 = fig3.colorbar(p3, ax=ax3)\n", - "_ = cbar3.ax.set_title('[MPa]', fontsize='small')" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/at_focused_bowl_AS/at_focused_bowl_AS.py b/examples/at_focused_bowl_AS.py similarity index 82% rename from examples/at_focused_bowl_AS/at_focused_bowl_AS.py rename to examples/at_focused_bowl_AS.py index 731db30af..2130cce88 100644 --- a/examples/at_focused_bowl_AS/at_focused_bowl_AS.py +++ b/examples/at_focused_bowl_AS.py @@ -1,236 +1,214 @@ -from copy import deepcopy - -import matplotlib.pyplot as plt -import numpy as np - -from kwave.data import Vector -from kwave.kgrid import kWaveGrid -from kwave.kmedium import kWaveMedium -from kwave.ksensor import kSensor -from kwave.ksource import kSource -from kwave.kspaceFirstOrderAS import kspaceFirstOrderASC -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions, SimulationType -from kwave.utils.filters import extract_amp_phase -from kwave.utils.kwave_array import kWaveArray -from kwave.utils.mapgen import focused_bowl_oneil -from kwave.utils.math import round_even -from kwave.utils.signals import create_cw_signals - -verbose: bool = False - -# medium parameters -c0: float = 1500.0 # sound speed [m/s] -rho0: float = 1000.0 # density [kg/m^3] - -# source parameters -source_f0 = 1.0e6 # source frequency [Hz] -source_roc = 30e-3 # bowl radius of curvature [m] -source_diameter = 30e-3 # bowl aperture diameter [m] -source_amp = np.array([1.0e6]) # source pressure [Pa] -source_phase = np.array([0.0]) # source phase [radians] - -# grid parameters -axial_size: float = 50.0e-3 # total grid size in the axial dimension [m] -lateral_size: float = 45.0e-3 # total grid size in the lateral dimension [m] - -# computational parameters -ppw: int = 3 # number of points per wavelength -t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state) -record_periods: int = 1 # number of periods to record -cfl: float = 0.05 # CFL number -source_x_offset: int = 20 # grid points to offset the source -bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points -upsampling_rate: int = 10 # density of integration points relative to grid - -# ========================================================================= -# RUN SIMULATION -# ========================================================================= - -# -------------------- -# GRID -# -------------------- - -# calculate the grid spacing based on the PPW and F0 -dx: float = c0 / (ppw * source_f0) # [m] - -# compute the size of the grid -Nx: int = round_even(axial_size / dx) + source_x_offset -Ny: int = round_even(lateral_size / dx) - -grid_size_points = Vector([Nx, Ny]) -grid_spacing_meters = Vector([dx, dx]) - -# create the k-space grid -kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) - -# compute points per temporal period -ppp: int = round(ppw / cfl) - -# compute corresponding time spacing -dt: float = 1.0 / (ppp * source_f0) - -# create the time array using an integer number of points per period -Nt: int = round(t_end / dt) -kgrid.setTime(Nt, dt) - -# calculate the actual CFL and PPW -if verbose: - print("PPW = " + str(c0 / (dx * source_f0))) - print("CFL = " + str(c0 * dt / dx)) - -# -------------------- -# SOURCE -# -------------------- - -source = kSource() - -# create time varying source -source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_amp, source_phase) - -# set arc position and orientation -arc_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0] -focus_pos = [kgrid.x_vec[-1].item(), 0] - -# create empty kWaveArray -karray = kWaveArray(axisymmetric=True, bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate, single_precision=True) - -# add bowl shaped element -karray.add_arc_element(arc_pos, source_roc, source_diameter, focus_pos) - -# assign binary mask -source.p_mask = karray.get_array_binary_mask(kgrid) - -# assign source signals -source.p = karray.get_distributed_source_signal(kgrid, source_sig) - -# -------------------- -# MEDIUM -# -------------------- - -# assign medium properties -medium = kWaveMedium(sound_speed=c0, density=rho0) - -# -------------------- -# SENSOR -# -------------------- - -sensor = kSensor() - -# set sensor mask to record central plane, not including the source point -sensor.mask = np.zeros((Nx, Ny), dtype=bool) -sensor.mask[(source_x_offset + 1) :, :] = True - -# record the pressure -sensor.record = ["p"] - -# record only the final few periods when the field is in steady state -sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1 - -# -------------------- -# SIMULATION -# -------------------- - -DATA_CAST = "single" - -simulation_options = SimulationOptions( - simulation_type=SimulationType.AXISYMMETRIC, - data_cast=DATA_CAST, - data_recast=False, - save_to_disk=True, - save_to_disk_exit=False, - pml_inside=False, -) - -execution_options = SimulationExecutionOptions(is_gpu_simulation=False, delete_data=False, verbose_level=2) - -# ========================================================================= -# RUN THE SIMULATION -# ========================================================================= - -sensor_data = kspaceFirstOrderASC( - medium=deepcopy(medium), - kgrid=deepcopy(kgrid), - source=deepcopy(source), - sensor=deepcopy(sensor), - simulation_options=simulation_options, - execution_options=execution_options, -) - -# extract amplitude from the sensor data -amp, _, _ = extract_amp_phase(sensor_data["p"].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") - -# reshape data -amp = np.reshape(amp, (Nx - (source_x_offset + 1), Ny), order="F") - -# extract pressure on axis -amp_on_axis = amp[:, 0] - -# define axis vectors for plotting -x_vec = np.squeeze(kgrid.x_vec[(source_x_offset + 1) :, :] - kgrid.x_vec[source_x_offset]) -y_vec = kgrid.y_vec - -# ========================================================================= -# ANALYTICAL SOLUTION -# ========================================================================= - -# calculate the wavenumber -knumber = 2.0 * np.pi * source_f0 / c0 - -# define radius and axis -x_max = (Nx - 1) * dx -delta_x = x_max / 10000.0 -x_ref = np.arange(0.0, x_max + delta_x, delta_x) - -# calculate analytical solution -p_ref_axial, _, _ = focused_bowl_oneil(source_roc, source_diameter, source_amp[0] / (c0 * rho0), source_f0, c0, rho0, axial_positions=x_ref) - -# calculate analytical solution at exactly the same points as k-Wave -p_ref_axial_kw, _, _ = focused_bowl_oneil( - source_roc, source_diameter, source_amp[0] / (c0 * rho0), source_f0, c0, rho0, axial_positions=x_vec -) - - -# ========================================================================= -# VISUALISATION -# ========================================================================= - -# plot the pressure along the focal axis of the piston -fig1, ax1 = plt.subplots(1, 1) -ax1.plot(1e3 * x_ref, 1e-6 * p_ref_axial, "k-", label="Exact") -ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") -ax1.legend() -ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") -ax1.set_xlim(0.0, 1e3 * axial_size) -ax1.set_ylim(0.0, 20) -ax1.grid() - -# get grid weights -grid_weights = karray.get_array_grid_weights(kgrid) - -# plot the source mask (pml is outside the grid in this example) -fig2, (ax2a, ax2b) = plt.subplots(1, 2) -ax2a.pcolormesh(1e3 * np.squeeze(kgrid.y_vec), 1e3 * np.squeeze(kgrid.x_vec), source.p_mask[:, :], shading="gouraud") -ax2a.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") -ax2b.pcolormesh(1e3 * np.squeeze(kgrid.y_vec), 1e3 * np.squeeze(kgrid.x_vec), grid_weights[:, :], shading="gouraud") -ax2b.set(xlabel="y [mm]", ylabel="x [mm]", title="Off-Grid Source Weights") - -# define axis vectors for plotting -yvec = np.squeeze(kgrid.y_vec) - kgrid.y_vec[0].item() -y_vec = np.hstack((-np.flip(yvec)[:-1], yvec)) -x_vec = np.squeeze(kgrid.x_vec[(source_x_offset + 1) :, :] - kgrid.x_vec[source_x_offset]) - -data = np.hstack((np.fliplr(amp[:, :-1]), amp)) / 1e6 -sp = np.hstack((np.fliplr(source.p_mask[:, :])[:, :-1], source.p_mask)) -gw = np.hstack((np.fliplr(grid_weights[:, :])[:, :-1], grid_weights)) - -# plot the pressure field -fig3, ax3 = plt.subplots(1, 1) -im3 = ax3.pcolormesh(1e3 * np.squeeze(y_vec), 1e3 * np.squeeze(x_vec), data, shading="gouraud") -ax3.set(xlabel="Lateral Position [mm]", ylabel="Axial Position [mm]", title="Pressure Field") -cbar3 = fig3.colorbar(im3, ax=ax3) -_ = cbar3.ax.set_title("[MPa]", fontsize="small") -ax3.invert_yaxis() - -# show figures -plt.show() +# %% [markdown] +# # Focused Bowl Transducer (Axisymmetric) +# Modelling a focused bowl transducer assuming axisymmetry, compared with O'Neil analytical solution. + +# %% +import matplotlib.pyplot as plt +import numpy as np + +from kwave.data import Vector +from kwave.kgrid import kWaveGrid +from kwave.kmedium import kWaveMedium +from kwave.ksensor import kSensor +from kwave.ksource import kSource +from kwave.kspaceFirstOrderAS import kspaceFirstOrderASC +from kwave.options.simulation_execution_options import SimulationExecutionOptions +from kwave.options.simulation_options import SimulationOptions +from kwave.utils.filters import extract_amp_phase +from kwave.utils.kwave_array import kWaveArray +from kwave.utils.mapgen import focused_bowl_oneil +from kwave.utils.math import round_even +from kwave.utils.signals import create_cw_signals + +# %% Parameters +verbose: bool = False + +# medium parameters +c0: float = 1500.0 # sound speed [m/s] +rho0: float = 1000.0 # density [kg/m^3] + +# source parameters +source_f0 = 1.0e6 # source frequency [Hz] +source_roc = 30e-3 # bowl radius of curvature [m] +source_diameter = 30e-3 # bowl aperture diameter [m] +source_amp = np.array([1.0e6]) # source pressure [Pa] +source_phase = np.array([0.0]) # source phase [radians] + +# grid parameters +axial_size: float = 50.0e-3 # total grid size in the axial dimension [m] +lateral_size: float = 45.0e-3 # total grid size in the lateral dimension [m] + +# computational parameters +ppw: int = 3 # number of points per wavelength +t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state) +record_periods: int = 1 # number of periods to record +cfl: float = 0.05 # CFL number +source_x_offset: int = 20 # grid points to offset the source +bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points +upsampling_rate: int = 10 # density of integration points relative to grid + +# %% Grid setup +# calculate the grid spacing based on the PPW and F0 +dx: float = c0 / (ppw * source_f0) # [m] + +# compute the size of the grid +Nx: int = round_even(axial_size / dx) + source_x_offset +Ny: int = round_even(lateral_size / dx) + +grid_size_points = Vector([Nx, Ny]) +grid_spacing_meters = Vector([dx, dx]) + +# create the k-space grid +kgrid = kWaveGrid(grid_size_points, grid_spacing_meters) + +# compute points per temporal period +ppp: int = round(ppw / cfl) + +# compute corresponding time spacing +dt: float = 1.0 / (ppp * source_f0) + +# create the time array using an integer number of points per period +Nt: int = round(t_end / dt) +kgrid.setTime(Nt, dt) + +# calculate the actual CFL and PPW +if verbose: + print("PPW = " + str(c0 / (dx * source_f0))) + print("CFL = " + str(c0 * dt / dx)) + +# %% Source, medium, sensor, and simulation +# -------------------- +# SOURCE +# -------------------- + +source = kSource() + +# create time varying source +source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_amp, source_phase) + +# set arc position and orientation +arc_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0] +focus_pos = [kgrid.x_vec[-1].item(), 0] + +# create empty kWaveArray +karray = kWaveArray(axisymmetric=True, bli_tolerance=bli_tolerance, upsampling_rate=upsampling_rate, single_precision=True) + +# add bowl shaped element +karray.add_arc_element(arc_pos, source_roc, source_diameter, focus_pos) + +# assign binary mask +source.p_mask = karray.get_array_binary_mask(kgrid) + +# assign source signals +source.p = karray.get_distributed_source_signal(kgrid, source_sig) + +# -------------------- +# MEDIUM +# -------------------- + +# assign medium properties +medium = kWaveMedium(sound_speed=c0, density=rho0) + +# -------------------- +# SENSOR +# -------------------- + +sensor = kSensor() + +# set sensor mask to record central plane, not including the source point +sensor.mask = np.zeros((Nx, Ny), dtype=bool) +sensor.mask[(source_x_offset + 1) :, :] = True + +# record the pressure +sensor.record = ["p"] + +# record only the final few periods when the field is in steady state +sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1 + +# -------------------- +# SIMULATION +# -------------------- + +# NOTE: Axisymmetric simulations require the legacy kspaceFirstOrderASC API +# until the new kspaceFirstOrder() supports the axisymmetric flag. +simulation_options = SimulationOptions(save_to_disk=True, data_cast="single") +execution_options = SimulationExecutionOptions(is_gpu_simulation=False) +sensor_data = kspaceFirstOrderASC( + kgrid=kgrid, + medium=medium, + source=source, + sensor=sensor, + simulation_options=simulation_options, + execution_options=execution_options, +) + +# extract amplitude from the sensor data +amp, _, _ = extract_amp_phase(sensor_data["p"].T, 1.0 / kgrid.dt, source_f0, dim=1, fft_padding=1, window="Rectangular") + +# reshape data +amp = np.reshape(amp, (Nx - (source_x_offset + 1), Ny), order="F") + +# extract pressure on axis +amp_on_axis = amp[:, 0] + +# define axis vectors for plotting +x_vec = np.squeeze(kgrid.x_vec[(source_x_offset + 1) :, :] - kgrid.x_vec[source_x_offset]) +y_vec = kgrid.y_vec + +# %% Analytical solution +# calculate the wavenumber +knumber = 2.0 * np.pi * source_f0 / c0 + +# define radius and axis +x_max = (Nx - 1) * dx +delta_x = x_max / 10000.0 +x_ref = np.arange(0.0, x_max + delta_x, delta_x) + +# calculate analytical solution +p_ref_axial, _, _ = focused_bowl_oneil(source_roc, source_diameter, source_amp[0] / (c0 * rho0), source_f0, c0, rho0, axial_positions=x_ref) + +# calculate analytical solution at exactly the same points as k-Wave +p_ref_axial_kw, _, _ = focused_bowl_oneil( + source_roc, source_diameter, source_amp[0] / (c0 * rho0), source_f0, c0, rho0, axial_positions=x_vec +) + +# %% Visualization +# plot the pressure along the focal axis of the piston +fig1, ax1 = plt.subplots(1, 1) +ax1.plot(1e3 * x_ref, 1e-6 * p_ref_axial, "k-", label="Exact") +ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, "b.", label="k-Wave") +ax1.legend() +ax1.set(xlabel="Axial Position [mm]", ylabel="Pressure [MPa]", title="Axial Pressure") +ax1.set_xlim(0.0, 1e3 * axial_size) +ax1.set_ylim(0.0, 20) +ax1.grid() + +# get grid weights +grid_weights = karray.get_array_grid_weights(kgrid) + +# plot the source mask (pml is outside the grid in this example) +fig2, (ax2a, ax2b) = plt.subplots(1, 2) +ax2a.pcolormesh(1e3 * np.squeeze(kgrid.y_vec), 1e3 * np.squeeze(kgrid.x_vec), source.p_mask[:, :], shading="gouraud") +ax2a.set(xlabel="y [mm]", ylabel="x [mm]", title="Source Mask") +ax2b.pcolormesh(1e3 * np.squeeze(kgrid.y_vec), 1e3 * np.squeeze(kgrid.x_vec), grid_weights[:, :], shading="gouraud") +ax2b.set(xlabel="y [mm]", ylabel="x [mm]", title="Off-Grid Source Weights") + +# define axis vectors for plotting +yvec = np.squeeze(kgrid.y_vec) - kgrid.y_vec[0].item() +y_vec = np.hstack((-np.flip(yvec)[:-1], yvec)) +x_vec = np.squeeze(kgrid.x_vec[(source_x_offset + 1) :, :] - kgrid.x_vec[source_x_offset]) + +data = np.hstack((np.fliplr(amp[:, :-1]), amp)) / 1e6 +sp = np.hstack((np.fliplr(source.p_mask[:, :])[:, :-1], source.p_mask)) +gw = np.hstack((np.fliplr(grid_weights[:, :])[:, :-1], grid_weights)) + +# plot the pressure field +fig3, ax3 = plt.subplots(1, 1) +im3 = ax3.pcolormesh(1e3 * np.squeeze(y_vec), 1e3 * np.squeeze(x_vec), data, shading="gouraud") +ax3.set(xlabel="Lateral Position [mm]", ylabel="Axial Position [mm]", title="Pressure Field") +cbar3 = fig3.colorbar(im3, ax=ax3) +_ = cbar3.ax.set_title("[MPa]", fontsize="small") +ax3.invert_yaxis() + +# show figures +plt.show() diff --git a/examples/at_focused_bowl_AS/README.md b/examples/at_focused_bowl_AS/README.md deleted file mode 100644 index 505a07eee..000000000 --- a/examples/at_focused_bowl_AS/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Modelling An Axisymmetric Focused Bowl Transducer Example - -This example models an axisymmetric focused bowl transducer. The _on-axis_ pressure is compared with an analytical solution calculated using `focused_bowl_oneil`. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_at_piston_and_bowl_transducers.php#heading6) \ No newline at end of file diff --git a/examples/at_focused_bowl_AS/at_focused_bowl_AS.ipynb b/examples/at_focused_bowl_AS/at_focused_bowl_AS.ipynb deleted file mode 100644 index 152ad9337..000000000 --- a/examples/at_focused_bowl_AS/at_focused_bowl_AS.ipynb +++ /dev/null @@ -1,384 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install git+https://github.com/waltsims/k-wave-python" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from copy import deepcopy\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrderAS import kspaceFirstOrderASC\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions, SimulationType\n", - "from kwave.utils.filters import extract_amp_phase\n", - "from kwave.utils.kwave_array import kWaveArray\n", - "from kwave.utils.mapgen import focused_bowl_oneil\n", - "from kwave.utils.math import round_even\n", - "from kwave.utils.signals import create_cw_signals" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Define simulation parameters" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "verbose: bool = False\n", - "\n", - "# medium parameters\n", - "c0: float = 1500.0 # sound speed [m/s]\n", - "rho0: float = 1000.0 # density [kg/m^3]\n", - "\n", - "# source parameters\n", - "source_f0 = 1.0e6 # source frequency [Hz]\n", - "source_roc = 30e-3 # bowl radius of curvature [m]\n", - "source_diameter = 30e-3 # bowl aperture diameter [m]\n", - "source_amp = np.array([1.0e6]) # source pressure [Pa]\n", - "source_phase = np.array([0.0]) # source phase [radians]\n", - "\n", - "# grid parameters\n", - "axial_size: float = 50.0e-3 # total grid size in the axial dimension [m]\n", - "lateral_size: float = 45.0e-3 # total grid size in the lateral dimension [m]\n", - "\n", - "# computational parameters\n", - "ppw: int = 3 # number of points per wavelength\n", - "t_end: float = 40e-6 # total compute time [s] (this must be long enough to reach steady state)\n", - "record_periods: int = 1 # number of periods to record\n", - "cfl: float = 0.05 # CFL number\n", - "source_x_offset: int = 20 # grid points to offset the source\n", - "bli_tolerance: float = 0.01 # tolerance for truncation of the off-grid source points\n", - "upsampling_rate: int = 10 # density of integration points relative to grid" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# calculate the grid spacing based on the PPW and F0\n", - "dx: float = c0 / (ppw * source_f0) # [m]\n", - "\n", - "# compute the size of the grid\n", - "Nx: int= round_even(axial_size / dx) + source_x_offset\n", - "Ny: int = round_even(lateral_size / dx)\n", - "\n", - "grid_size_points = Vector([Nx, Ny])\n", - "grid_spacing_meters = Vector([dx, dx])\n", - "\n", - "# create the k-space grid\n", - "kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)\n", - "\n", - "# compute points per temporal period\n", - "ppp: int = round(ppw / cfl)\n", - "\n", - "# compute corresponding time spacing\n", - "dt: float = 1.0 / (ppp * source_f0)\n", - "\n", - "# create the time array using an integer number of points per period\n", - "Nt: int = round(t_end / dt)\n", - "kgrid.setTime(Nt, dt)\n", - "\n", - "# calculate the actual CFL and PPW\n", - "if verbose:\n", - " print('PPW = ' + str(c0 / (dx * source_f0)))\n", - " print('CFL = ' + str(c0 * dt / dx))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Assign medium properties" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "medium = kWaveMedium(sound_speed=c0, density=rho0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Set up the source" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "source = kSource()\n", - "\n", - "# create time varying source\n", - "source_sig = create_cw_signals(np.squeeze(kgrid.t_array), source_f0, source_amp,\n", - " source_phase)\n", - "\n", - "# set arc position and orientation\n", - "arc_pos = [kgrid.x_vec[0].item() + source_x_offset * kgrid.dx, 0]\n", - "focus_pos = [kgrid.x_vec[-1].item(), 0]\n", - "\n", - "# create empty kWaveArray\n", - "karray = kWaveArray(axisymmetric=True,\n", - " bli_tolerance=bli_tolerance,\n", - " upsampling_rate=upsampling_rate,\n", - " single_precision=True)\n", - "\n", - "# add bowl shaped element\n", - "karray.add_arc_element(arc_pos, source_roc, source_diameter, focus_pos)\n", - "\n", - "# assign binary mask\n", - "source.p_mask = karray.get_array_binary_mask(kgrid)\n", - "\n", - "# assign source signals\n", - "source.p = karray.get_distributed_source_signal(kgrid, source_sig)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Define the sensor" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sensor = kSensor()\n", - "\n", - "# set sensor mask to record central plane, not including the source point\n", - "sensor.mask = np.zeros((Nx, Ny), dtype=bool)\n", - "sensor.mask[(source_x_offset + 1):, :] = True\n", - "\n", - "# record the pressure\n", - "sensor.record = ['p']\n", - "\n", - "# record only the final few periods when the field is in steady state\n", - "sensor.record_start_index = kgrid.Nt - (record_periods * ppp) + 1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Simulation options. Note that for axisymmetric computations the simulation type must be set and the declared that it will run on the CPU." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "simulation_options = SimulationOptions(\n", - " simulation_type=SimulationType.AXISYMMETRIC,\n", - " data_cast='single',\n", - " data_recast=False,\n", - " save_to_disk=True,\n", - " save_to_disk_exit=False,\n", - " pml_inside=False)\n", - "\n", - "execution_options = SimulationExecutionOptions(\n", - " is_gpu_simulation=False,\n", - " delete_data=False,\n", - " verbose_level=2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Run the simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sensor_data = kspaceFirstOrderASC(medium=deepcopy(medium),\n", - " kgrid=deepcopy(kgrid),\n", - " source=deepcopy(source),\n", - " sensor=deepcopy(sensor),\n", - " simulation_options=simulation_options,\n", - " execution_options=execution_options)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Post-process the data, and calculate the on-axis analytical solution" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# extract amplitude from the sensor data\n", - "amp, _, _ = extract_amp_phase(sensor_data['p'].T, 1.0 / kgrid.dt, source_f0,\n", - " dim=1, fft_padding=1, window='Rectangular')\n", - "\n", - "# reshape data\n", - "amp = np.reshape(amp, (Nx - (source_x_offset+1), Ny), order='F')\n", - "\n", - "# extract pressure on axis\n", - "amp_on_axis = amp[:, 0]\n", - "\n", - "# define axis vectors for plotting\n", - "x_vec = np.squeeze(kgrid.x_vec[(source_x_offset + 1):, :] - kgrid.x_vec[source_x_offset])\n", - "y_vec = kgrid.y_vec\n", - "\n", - "# calculate the wavenumber\n", - "knumber = 2.0 * np.pi * source_f0 / c0\n", - "\n", - "# define beam axis for plotting\n", - "x_max = (Nx-1) * dx\n", - "delta_x = x_max / 10000.0\n", - "x_ref = np.arange(0.0, x_max + delta_x, delta_x)\n", - "\n", - "# calculate analytical solution\n", - "p_ref_axial, _, _ = focused_bowl_oneil(source_roc,\n", - " source_diameter,\n", - " source_amp[0] / (c0 * rho0),\n", - " source_f0,\n", - " c0,\n", - " rho0,\n", - " axial_positions=x_ref)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Visualize solutions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# =========================================================================\n", - "# VISUALISATION\n", - "# =========================================================================\n", - "\n", - "# plot the pressure along the focal axis of the piston\n", - "fig1, ax1 = plt.subplots(1, 1)\n", - "ax1.plot(1e3 * x_ref, 1e-6 * p_ref_axial, 'k-', label='Exact')\n", - "ax1.plot(1e3 * x_vec, 1e-6 * amp_on_axis, 'b.', label='k-Wave')\n", - "ax1.legend()\n", - "ax1.set(xlabel='Axial Position [mm]',\n", - " ylabel='Pressure [MPa]',\n", - " title='Axial Pressure')\n", - "ax1.set_xlim(0.0, 1e3 * axial_size)\n", - "ax1.set_ylim(0.0, 20)\n", - "ax1.grid()\n", - "\n", - "# get grid weights\n", - "grid_weights = karray.get_array_grid_weights(kgrid)\n", - "\n", - "# define axis vectors for plotting\n", - "yvec = np.squeeze(kgrid.y_vec) - kgrid.y_vec[0].item()\n", - "y_vec = np.hstack((-np.flip(yvec)[:-1], yvec))\n", - "x_vec = np.squeeze(kgrid.x_vec[(source_x_offset + 1):, :] - kgrid.x_vec[source_x_offset])\n", - "\n", - "data = np.hstack((np.fliplr(amp[:, :-1]), amp)) / 1e6\n", - "sp = np.hstack((np.fliplr(source.p_mask[:, :-1]), source.p_mask))\n", - "gw = np.hstack((np.fliplr(grid_weights[:, :-1]), grid_weights))\n", - "\n", - "fig3, (ax3a, ax3b) = plt.subplots(1, 2)\n", - "ax3a.pcolormesh(1e3 * np.squeeze(y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec[:, :] - kgrid.x_vec[source_x_offset]),\n", - " sp,\n", - " shading='gouraud')\n", - "ax3a.set(xlabel='y [mm]',\n", - " ylabel='x [mm]',\n", - " title='Source Mask')\n", - "ax3b.pcolormesh(1e3 * np.squeeze(y_vec),\n", - " 1e3 * np.squeeze(kgrid.x_vec[:, :] - kgrid.x_vec[source_x_offset]),\n", - " gw,\n", - " shading='gouraud')\n", - "ax3b.set(xlabel='y [mm]',\n", - " ylabel='x [mm]',\n", - " title='Off-Grid Source Weights')\n", - "fig3.tight_layout(pad=1.2)\n", - "\n", - "# plot the pressure field\n", - "fig4, ax4 = plt.subplots(1, 1)\n", - "ax4.pcolormesh(1e3 * np.squeeze(y_vec),\n", - " 1e3 * np.squeeze(x_vec),\n", - " data,\n", - " shading='gouraud')\n", - "ax4.set(xlabel='Lateral Position [mm]',\n", - " ylabel='Axial Position [mm]',\n", - " title='Pressure Field')\n", - "ax4.invert_yaxis()\n", - "\n", - "# show figures\n", - "plt.show()" - ] - } - ], - "metadata": { - "colab": { - "authorship_tag": "ABX9TyPvgfi818S8IbWGlN6C6Tmi", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/at_linear_array_transducer/at_linear_array_transducer.py b/examples/at_linear_array_transducer.py similarity index 81% rename from examples/at_linear_array_transducer/at_linear_array_transducer.py rename to examples/at_linear_array_transducer.py index f48cd58b3..c17f31ef0 100644 --- a/examples/at_linear_array_transducer/at_linear_array_transducer.py +++ b/examples/at_linear_array_transducer.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # Linear Array Transducer +# Simulating a focused linear array transducer in 3D with time-delayed elements. + +# %% import matplotlib.pyplot as plt import numpy as np @@ -6,9 +11,7 @@ from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor from kwave.ksource import kSource -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3DC -from kwave.kWaveSimulation import SimulationOptions -from kwave.options.simulation_execution_options import SimulationExecutionOptions +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.utils.colormap import get_color_map from kwave.utils.kwave_array import kWaveArray from kwave.utils.plot import voxel_plot @@ -16,6 +19,7 @@ def main(): + # %% Parameters and grid c0 = 1500 rho0 = 1000 source_f0 = 1e6 @@ -43,7 +47,7 @@ def main(): kgrid = kWaveGrid([Nx, Ny, Nz], [dx, dx, dx]) kgrid.makeTime(c0, cfl, t_end) - # SOURCE + # %% Source if element_num % 2 != 0: ids = np.arange(1, element_num + 1) - np.ceil(element_num / 2) else: @@ -65,6 +69,7 @@ def main(): voxel_plot(np.single(source.p_mask)) source.p = karray.get_distributed_source_signal(kgrid, source_sig) + # %% Medium, sensor, and simulation # MEDIUM medium = kWaveMedium(sound_speed=c0, density=rho0) @@ -74,22 +79,20 @@ def main(): sensor = kSensor(sensor_mask, record=["p_max"]) # SIMULATION - simulation_options = SimulationOptions( - pml_auto=True, - pml_inside=False, - save_to_disk=True, - data_cast="single", - ) - - execution_options = SimulationExecutionOptions(is_gpu_simulation=True) - - sensor_data = kspaceFirstOrder3DC( - kgrid=kgrid, medium=medium, source=source, sensor=sensor, simulation_options=simulation_options, execution_options=execution_options + # NOTE: pml_inside=False, data_cast="single" not supported in new API + sensor_data = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + pml_size="auto", + backend="cpp", + device="gpu", ) p_max = np.reshape(sensor_data["p_max"], (Nx, Nz), order="F") - # VISUALISATION + # %% Visualization plt.figure() plt.imshow( 1e-6 * p_max, diff --git a/examples/at_linear_array_transducer/README.md b/examples/at_linear_array_transducer/README.md deleted file mode 100644 index 0b4793587..000000000 --- a/examples/at_linear_array_transducer/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Modelling A Linear Array Transducer Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/at_linear_array_transducer/at_linear_array_transducer.ipynb) - -This example provides a demonstration of using the kWaveArray class to define a linear array transducer with 15 rectangular elements. Electronic focusing is then used to transmit an ultrasound pulse. It builds on the [Defining A Source Using An Array Transducer Example](../at_array_as_source/). - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_at_linear_array_transducer.php). \ No newline at end of file diff --git a/examples/at_linear_array_transducer/at_linear_array_transducer.ipynb b/examples/at_linear_array_transducer/at_linear_array_transducer.ipynb deleted file mode 100644 index a5a6207bc..000000000 --- a/examples/at_linear_array_transducer/at_linear_array_transducer.ipynb +++ /dev/null @@ -1,201 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "import kwave.data\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3DC\n", - "from kwave.kWaveSimulation import SimulationOptions\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.utils.colormap import get_color_map\n", - "from kwave.utils.kwave_array import kWaveArray\n", - "from kwave.utils.plot import voxel_plot\n", - "from kwave.utils.signals import tone_burst" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define constants\n", - "c0 = 1500\n", - "rho0 = 1000\n", - "source_f0 = 1e6\n", - "source_amp = 1e6\n", - "source_cycles = 5\n", - "source_focus = 20e-3\n", - "element_num = 15\n", - "element_width = 1e-3\n", - "element_length = 10e-3\n", - "element_pitch = 2e-3\n", - "translation = kwave.data.Vector([5e-3, 0, 8e-3])\n", - "rotation = kwave.data.Vector([0, 20, 0])\n", - "grid_size_x = 40e-3\n", - "grid_size_y = 20e-3\n", - "grid_size_z = 40e-3\n", - "ppw = 3\n", - "t_end = 35e-6\n", - "cfl = 0.5\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "# GRID\n", - "dx = c0 / (ppw * source_f0)\n", - "Nx = round(grid_size_x / dx)\n", - "Ny = round(grid_size_y / dx)\n", - "Nz = round(grid_size_z / dx)\n", - "kgrid = kWaveGrid([Nx, Ny, Nz], [dx, dx, dx])\n", - "kgrid.makeTime(c0, cfl, t_end)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# SOURCE\n", - "if element_num % 2 != 0:\n", - " ids = np.arange(1, element_num + 1) - np.ceil(element_num / 2)\n", - "else:\n", - " ids = np.arange(1, element_num + 1) - (element_num + 1) / 2\n", - "\n", - "time_delays = -(np.sqrt((ids * element_pitch) ** 2 + source_focus ** 2) - source_focus) / c0\n", - "time_delays = time_delays - min(time_delays)\n", - "\n", - "source_sig = source_amp * tone_burst(1 / kgrid.dt, source_f0, source_cycles,\n", - " signal_offset=np.round(time_delays / kgrid.dt).astype(int))\n", - "karray = kWaveArray(bli_tolerance=0.05, upsampling_rate=10)\n", - "\n", - "for ind in range(element_num):\n", - " x_pos = 0 - (element_num * element_pitch / 2 - element_pitch / 2) + ind * element_pitch\n", - " karray.add_rect_element([x_pos, 0, kgrid.z_vec[0][0]], element_width, element_length, rotation)\n", - "\n", - "karray.set_array_position(translation, rotation)\n", - "source = kSource()\n", - "source.p_mask = karray.get_array_binary_mask(kgrid)\n", - "%matplotlib inline\n", - "voxel_plot(np.single(source.p_mask))\n", - "source.p = karray.get_distributed_source_signal(kgrid, source_sig)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# MEDIUM\n", - "medium = kWaveMedium(sound_speed=c0, density=rho0)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# SENSOR\n", - "sensor_mask = np.zeros((Nx, Ny, Nz))\n", - "sensor_mask[:, Ny // 2, :] = 1\n", - "sensor = kSensor(sensor_mask, record=['p_max'])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# SIMULATION\n", - "simulation_options = SimulationOptions(\n", - " pml_auto=True,\n", - " pml_inside=False,\n", - " save_to_disk=True,\n", - " data_cast='single',\n", - ")\n", - "\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True)\n", - "\n", - "sensor_data = kspaceFirstOrder3DC(kgrid=kgrid, medium=medium, source=source, sensor=sensor,\n", - " simulation_options=simulation_options, execution_options=execution_options)\n", - "\n", - "p_max = np.reshape(sensor_data['p_max'], (Nx, Nz), order='F')\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# VISUALISATION\n", - "plt.figure()\n", - "plt.imshow(1e-6 * p_max, extent=[1e3 * kgrid.x_vec[0][0], 1e3 * kgrid.x_vec[-1][0], 1e3 * kgrid.z_vec[0][0],\n", - " 1e3 * kgrid.z_vec[-1][0]], aspect='auto', cmap=get_color_map())\n", - "plt.xlabel('z-position [mm]')\n", - "plt.ylabel('x-position [mm]')\n", - "plt.title('Pressure Field')\n", - "plt.colorbar(label='[MPa]')\n", - "plt.show()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/checkpointing/README.md b/examples/checkpointing/README.md deleted file mode 100644 index c0f068010..000000000 --- a/examples/checkpointing/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Checkpointing - -This example demonstrates how to checkpoint simulations. - -It uses the same simulation parameters as found in "3D FFT Reconstruction For A Planar Sensor". \ No newline at end of file diff --git a/examples/checkpointing/checkpoint.py b/examples/checkpointing/checkpoint.py index 1f4590dfd..cd89cac7c 100644 --- a/examples/checkpointing/checkpoint.py +++ b/examples/checkpointing/checkpoint.py @@ -1,3 +1,7 @@ +# %% [markdown] +# # Checkpointing Example +# Demonstrates how to use k-Wave's checkpointing feature to resume a simulation from a saved state. + from copy import copy from pathlib import Path from tempfile import TemporaryDirectory diff --git a/examples/ivp_1D_simulation.py b/examples/ivp_1D_simulation.py index 41e83d26d..daffc29c3 100644 --- a/examples/ivp_1D_simulation.py +++ b/examples/ivp_1D_simulation.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # 1D Initial Value Problem +# Heterogeneous medium with reflections at impedance boundaries. + +# %% """ 1D Initial Value Problem — heterogeneous medium with reflections. @@ -15,7 +20,7 @@ from kwave.ksource import kSource from kwave.kspaceFirstOrder import kspaceFirstOrder -# -- Grid -- +# %% Grid and medium Nx = 512 dx = 0.05e-3 # [m] kgrid = kWaveGrid(Vector([Nx]), Vector([dx])) @@ -32,7 +37,7 @@ # -- Time stepping -- kgrid.makeTime(sound_speed, cfl=0.3) -# -- Source: smooth sinusoidal pulse -- +# %% Source and sensor source = kSource() source.p0 = np.zeros(Nx) x0, width = 280, 100 @@ -45,12 +50,12 @@ sensor_mask[3 * Nx // 4] = 1 # right sensor sensor = kSensor(mask=sensor_mask) -# -- Run -- +# %% Run simulation result = kspaceFirstOrder(kgrid, medium, source, sensor, backend="python") print(f"Sensor data shape: {result['p'].shape}") # (2, Nt) -# -- Plot -- +# %% Visualization t_us = np.arange(kgrid.Nt) * float(kgrid.dt) * 1e6 x_mm = np.arange(Nx) * dx * 1e3 diff --git a/examples/ivp_photoacoustic_waveforms/ivp_photoacoustic_waveforms.py b/examples/ivp_photoacoustic_waveforms.py similarity index 52% rename from examples/ivp_photoacoustic_waveforms/ivp_photoacoustic_waveforms.py rename to examples/ivp_photoacoustic_waveforms.py index b7407a6cd..df9e29223 100644 --- a/examples/ivp_photoacoustic_waveforms/ivp_photoacoustic_waveforms.py +++ b/examples/ivp_photoacoustic_waveforms.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # Photoacoustic Waveforms +# Comparing photoacoustic waveforms from 2D disc and 3D ball sources. + +# %% import matplotlib.pyplot as plt import numpy as np @@ -6,13 +11,11 @@ from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor from kwave.ksource import kSource -from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.utils.data import scale_SI from kwave.utils.mapgen import make_ball, make_disc +# %% Parameters # number of grid points in the x (row) direction Nx: int = 64 @@ -35,44 +38,7 @@ dt: float = 2e-9 # [s] t_end: float = 300e-9 # [s] -# # create the medium -# medium1 = kWaveMedium(sound_speed=sound_speed) - -# # create the computational grid -# kgrid1 = kWaveGrid([Nx], [dx]) - -# # create the time array -# kgrid1.setTime(np.round(t_end / dt), dt) - -# # create initial pressure distribution -# source1 = kSource() -# source1.p0[Nx//2 - source_radius:Nx//2 + source_radius] = 1.0 - -# # define a single sensor point -# sensor1 = kSensor() -# sensor1.mask = np.zeros((Nx,), dtype=bool) -# sensor1.mask[Nx // 2 + source_sensor_distance] = True - -# simulation_options1 = SimulationOptions( -# data_cast='single', -# save_to_disk=True) - -# execution_options1 = SimulationExecutionOptions( -# is_gpu_simulation=True, -# delete_data=False, -# verbose_level=2) - -# # run the simulation -# sensor_data_1D = kspaceFirstOrder1D( -# medium=medium1, -# kgrid=kgrid1, -# source=source1, -# sensor=sensor1, -# simulation_options=simulation_options1, -# execution_options=execution_options1) - -####### - +# %% 2D simulation # medium medium2 = kWaveMedium(sound_speed=1500) # create the k-space grid @@ -98,22 +64,18 @@ source2 = kSource() source2.p0 = make_disc(Vector([Nx, Nx]), Vector([Nx // 2, Nx // 2]), source_radius, plot_disc=False) -simulation_options2 = SimulationOptions(data_cast="single", save_to_disk=True) - -execution_options2 = SimulationExecutionOptions(is_gpu_simulation=True, delete_data=False, verbose_level=2) - +# NOTE: data_cast="single" not supported in new API # run the simulation -sensor_data_2D = kspaceFirstOrder2D( - medium=medium2, - kgrid=kgrid2, - source=source2, - sensor=sensor2, - simulation_options=simulation_options2, - execution_options=execution_options2, +sensor_data_2D = kspaceFirstOrder( + kgrid2, + medium2, + source2, + sensor2, + backend="cpp", + device="gpu", ) -############ - +# %% 3D simulation # medium medium3 = kWaveMedium(sound_speed=1500) @@ -139,25 +101,25 @@ source3 = kSource() source3.p0 = make_ball(Vector([Nx, Nx, Nx]), Vector([Nx // 2, Nx // 2, Nx // 2]), source_radius) -simulation_options3 = SimulationOptions(data_cast="single", save_to_disk=True) - -execution_options3 = SimulationExecutionOptions(is_gpu_simulation=True, delete_data=False, verbose_level=2) - +# NOTE: data_cast="single" not supported in new API # run the simulation -sensor_data_3D = kspaceFirstOrder3D( - medium=medium3, - kgrid=kgrid3, - source=source3, - sensor=sensor3, - simulation_options=simulation_options3, - execution_options=execution_options3, +sensor_data_3D = kspaceFirstOrder( + kgrid3, + medium3, + source3, + sensor3, + backend="cpp", + device="gpu", ) +# %% Visualization # plot the simulations t_sc, t_scale, t_prefix, _ = scale_SI(t_end) _, ax1 = plt.subplots() -ax1.plot(np.squeeze(kgrid2.t_array * t_scale), sensor_data_2D["p"] / np.max(np.abs(sensor_data_2D["p"])), "r-", label="2D") -ax1.plot(np.squeeze(kgrid3.t_array * t_scale), sensor_data_3D["p"] / np.max(np.abs(sensor_data_3D["p"])), "k-", label="3D") +p_2D = sensor_data_2D["p"].squeeze() +p_3D = sensor_data_3D["p"].squeeze() +ax1.plot(np.squeeze(kgrid2.t_array * t_scale), p_2D / np.max(np.abs(p_2D)), "r-", label="2D") +ax1.plot(np.squeeze(kgrid3.t_array * t_scale), p_3D / np.max(np.abs(p_3D)), "k-", label="3D") ax1.set(xlabel=f"Time [{t_prefix}s]", ylabel="Recorded Pressure [au]") ax1.grid(True) ax1.legend(loc="upper right") diff --git a/examples/ivp_photoacoustic_waveforms/README.md b/examples/ivp_photoacoustic_waveforms/README.md deleted file mode 100644 index 625650ac1..000000000 --- a/examples/ivp_photoacoustic_waveforms/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Initial Value Problem: Photoacoustic Waveforms in 2D and 3D - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/ivp_photoacoustic_waveforms/ivp_photoacoustic_waveforms.ipynb) - -See the k-wave example [here](http://www.k-wave.org/documentation/example_ivp_photoacoustic_waveforms.php), which includes one-dimensional propagation as well. - -> [!WARNING] -> As there is no one-dimensional simulation, the example does not fully recreate the k-wave example. It underable to show how a 1D wave is different from 2D and 3D waves. - -## Rational - -The purpose of this example is to show that the dimensions of a system influence characteristics of the propagation. A point source in two-dimensions is represented by an infinite line source in three-dimensions. - -In two-dimensions there is cylindrical spreading, This means the acoustic energy is inversely proportional to radius, $r$, and the acoustic pressure decays as $1/\sqrt r$. In three-dimensions there is spherical spreading, so the energy is spread over $r^2$, and the pressure decays as ${1/r}$. - -Photoacoustic waves in 3D have [compact support](https://en.wikipedia.org/wiki/Support_(mathematics)#Compact_support). This means they decay to zero rapidly, whereas a waveform in 2D does not have this property. As an infinite line source in 3D, there will always be some signal arriving to the detector from some (increasingly distant) part of the line source. diff --git a/examples/ivp_photoacoustic_waveforms/ivp_photoacoustic_waveforms.ipynb b/examples/ivp_photoacoustic_waveforms/ivp_photoacoustic_waveforms.ipynb deleted file mode 100644 index 42f832b74..000000000 --- a/examples/ivp_photoacoustic_waveforms/ivp_photoacoustic_waveforms.ipynb +++ /dev/null @@ -1,284 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "11cd3967", - "metadata": {}, - "source": [ - "# Initial Value Problem: Photoacoustic Waveforms\n", - "\n", - "First load the modules needed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c11efc20", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python" - ] - }, - { - "cell_type": "markdown", - "id": "35bbf8ce", - "metadata": {}, - "source": [ - "Now import the libraries we need" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1fe4153f", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.data import scale_SI\n", - "from kwave.utils.mapgen import make_ball, make_disc" - ] - }, - { - "cell_type": "markdown", - "id": "96d23a4d", - "metadata": {}, - "source": [ - "Next define the problem in terms of the domain dimension, size, source position, etc." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a7e5f5b5", - "metadata": {}, - "outputs": [], - "source": [ - "Nx = 64 # number of grid points in the x (row) direction\n", - "\n", - "x = 1e-3 # size of the domain in the x direction [m]\n", - "\n", - "dx = x / Nx # grid point spacing in the x direction [m]\n", - "\n", - "sound_speed = 1500\n", - "\n", - "# size of the initial pressure distribution\n", - "source_radius = 2 # [grid points]\n", - "\n", - "# distance between the centre of the source and the sensor\n", - "source_sensor_distance = 10 # [grid points]\n", - "\n", - "# time array\n", - "dt = 2e-9 # [s]\n", - "t_end = 300e-9 # [s]" - ] - }, - { - "cell_type": "markdown", - "id": "87aa5a24", - "metadata": {}, - "source": [ - "Code for 2d simulations" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "964309b3", - "metadata": {}, - "outputs": [], - "source": [ - "medium2 = kWaveMedium(sound_speed=sound_speed)\n", - "\n", - "# create the k-space grid\n", - "kgrid2 = kWaveGrid([Nx, Nx], [dx, dx])\n", - "\n", - "# create the time array using an integer number of points per period\n", - "kgrid2.setTime(int(np.round(t_end / dt)), dt)\n", - "\n", - "# create instance of a sensor\n", - "sensor2 = kSensor()\n", - "\n", - "# set sensor mask: the mask says at which points data should be recorded\n", - "sensor2.mask = np.zeros((Nx, Nx), dtype=bool)\n", - "\n", - "# define a single sensor point\n", - "sensor2.mask[Nx // 2 + source_sensor_distance, Nx // 2] = True\n", - "\n", - "# set the record type: record the pressure waveform\n", - "sensor2.record = ['p']\n", - "\n", - "# make a source object\n", - "source2 = kSource()\n", - "source2.p0 = make_disc(Vector([Nx, Nx]), Vector([Nx // 2, Nx // 2]), source_radius, plot_disc=False)" - ] - }, - { - "cell_type": "markdown", - "id": "a53f8a88", - "metadata": {}, - "source": [ - "simulation and execution options" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "37de7ce4", - "metadata": {}, - "outputs": [], - "source": [ - "simulation_options = SimulationOptions(\n", - " data_cast='single',\n", - " save_to_disk=True)\n", - "\n", - "execution_options = SimulationExecutionOptions(\n", - " is_gpu_simulation=True,\n", - " delete_data=False,\n", - " verbose_level=2)" - ] - }, - { - "cell_type": "markdown", - "id": "315e699c", - "metadata": {}, - "source": [ - "Now run the simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b4e9d359", - "metadata": {}, - "outputs": [], - "source": [ - "# run the simulation\n", - "sensor_data_2D = kspaceFirstOrder2D(\n", - " medium=medium2,\n", - " kgrid=kgrid2,\n", - " source=source2,\n", - " sensor=sensor2,\n", - " simulation_options=simulation_options,\n", - " execution_options=execution_options)" - ] - }, - { - "cell_type": "markdown", - "id": "f25433e0", - "metadata": {}, - "source": [ - "# 3D Simulations" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "76905e44", - "metadata": {}, - "outputs": [], - "source": [ - "medium3 = kWaveMedium(sound_speed=1500)\n", - "\n", - "# create the k-space grid\n", - "kgrid3 = kWaveGrid([Nx, Nx, Nx], [dx, dx, dx])\n", - "\n", - "# create the time array using an integer number of points per period\n", - "kgrid3.setTime(int(np.round(t_end / dt)), dt)\n", - "\n", - "# create instance of a sensor\n", - "sensor3 = kSensor()\n", - "\n", - "# set sensor mask: the mask says at which points data should be recorded\n", - "sensor3.mask = np.zeros((Nx, Nx, Nx), dtype=bool)\n", - "\n", - "# define a single sensor point\n", - "sensor3.mask[Nx // 2 + source_sensor_distance, Nx // 2, Nx // 2] = True\n", - "\n", - "# set the record type: record the pressure waveform\n", - "sensor3.record = ['p']\n", - "\n", - "# make a source object\n", - "source3 = kSource()\n", - "source3.p0 = make_ball(Vector([Nx, Nx, Nx]), Vector([Nx // 2, Nx // 2, Nx // 2]), source_radius)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cfce9e85", - "metadata": {}, - "outputs": [], - "source": [ - "sensor_data_3D = kspaceFirstOrder3D(\n", - " medium=medium3,\n", - " kgrid=kgrid3,\n", - " source=source3,\n", - " sensor=sensor3,\n", - " simulation_options=simulation_options,\n", - " execution_options=execution_options)" - ] - }, - { - "cell_type": "markdown", - "id": "1626ec9b", - "metadata": {}, - "source": [ - "Plot the data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "507443fa", - "metadata": {}, - "outputs": [], - "source": [ - "t_sc, t_scale, t_prefix, _ = scale_SI(t_end)\n", - "_, ax1 = plt.subplots()\n", - "ax1.plot(np.squeeze(kgrid2.t_array * t_scale), sensor_data_2D['p'] / np.max(np.abs(sensor_data_2D['p'])), 'r-')\n", - "ax1.plot(np.squeeze(kgrid3.t_array * t_scale), sensor_data_3D[\"p\"] / np.max(np.abs(sensor_data_3D[\"p\"])), \"k-\", label=\"3D\")\n", - "ax1.set(xlabel= f\"Time [{t_prefix}s]\", ylabel='Recorded Pressure [au]')\n", - "ax1.grid(True)\n", - "plt.show()" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/na_controlling_the_pml/README.md b/examples/na_controlling_the_pml/README.md deleted file mode 100644 index 45c33b684..000000000 --- a/examples/na_controlling_the_pml/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Using An Ultrasound Transducer As A Sensor Example - - - Open In Colab - - -# Running the example - -Please have a look at the `example_na_controlling_the_pml.ipynb` notebook file to see the example in action. - -## Preparing the Matlab reference files - -We can verify the correctness of Python implementation by saving intermediate/final variable states from the Matlab script -and comparing them against Python version. Saving the variable states can be done manually by running the Matlab script, -pausing it in appropriate places and saving the variables using `save(...)` method. However, this is very involved process. -To automate the process a bit, we followed the steps below. - -1. Grab the original example file from K-wave repository (link: [example_na_controlling_the_PML.m](https://github.com/ucl-bug/k-wave/blob/main/k-Wave/examples/example_na_controlling_the_PML.m)) -2. Modify the file such that we are running for all scenarios (4 different simulation cases) and saving the output sensor data + visualized figures each time. We saved the modified script in the `modified_matlab_example.m` file. -3. Run the modified file from the step above. Now we should have 4x `.mat` and `.png` files for the captured sensor data and visualized figure, respectively. -4. We check against these files in the `example_na_controlling_the_pml.ipynb` notebook. diff --git a/examples/na_controlling_the_pml/modified_matlab_example.m b/examples/na_controlling_the_pml/modified_matlab_example.m deleted file mode 100644 index b33f687cc..000000000 --- a/examples/na_controlling_the_pml/modified_matlab_example.m +++ /dev/null @@ -1,68 +0,0 @@ -clearvars; - -% create the computational grid -Nx = 128; % number of grid points in the x (row) direction -Ny = 128; % number of grid points in the y (column) direction -dx = 0.1e-3; % grid point spacing in the x direction [m] -dy = 0.1e-3; % grid point spacing in the y direction [m] -kgrid = kWaveGrid(Nx, dx, Ny, dy); - -% define the properties of the propagation medium -medium.sound_speed = 1500; % [m/s] - -% create initial pressure distribution using makeDisc -disc_magnitude = 5; % [Pa] -disc_x_pos = 50; % [grid points] -disc_y_pos = 50; % [grid points] -disc_radius = 8; % [grid points] -disc_1 = disc_magnitude * makeDisc(Nx, Ny, disc_x_pos, disc_y_pos, disc_radius); - -disc_magnitude = 3; % [Pa] -disc_x_pos = 80; % [grid points] -disc_y_pos = 60; % [grid points] -disc_radius = 5; % [grid points] -disc_2 = disc_magnitude * makeDisc(Nx, Ny, disc_x_pos, disc_y_pos, disc_radius); - -source.p0 = disc_1 + disc_2; - -% define a centered circular sensor -sensor_radius = 4e-3; % [m] -num_sensor_points = 50; -sensor.mask = makeCartCircle(sensor_radius, num_sensor_points); -sensor.mask = cart2grid(kgrid, sensor.mask, false); % otherwise cpu computation will not work - -% Example 1: PML with no absorption -input_args = {'PMLAlpha', 0}; -run_simulation_and_record_input_output('example_1', kgrid, medium, source, sensor, input_args); - -% Example 2: PML with the absorption value set too high -input_args = {'PMLAlpha', 1e6}; -run_simulation_and_record_input_output('example_2', kgrid, medium, source, sensor, input_args); - -% Example 3: partially effective PML -input_args = {'PMLSize', 2}; -run_simulation_and_record_input_output('example_3', kgrid, medium, source, sensor, input_args); - -% Example 4: PML set to be outside the computational domain -input_args = {'PMLInside', false}; -run_simulation_and_record_input_output('example_4', kgrid, medium, source, sensor, input_args); - - -% define a new matlab function -function run_simulation_and_record_input_output(name, kgrid, medium, source, sensor, input_args) - - sensor_data = kspaceFirstOrder2DC(kgrid, medium, source, sensor, input_args{:}); - - % plot the simulated sensor data - figure; - imagesc(sensor_data, [-1, 1]); - colormap(getColorMap); - ylabel('Sensor Position'); - xlabel('Time Step'); - colorbar; - - save(sprintf('%s.mat', name), 'sensor_data', 'input_args'); - - % save the figure - saveas(gcf, sprintf('%s.png', name)); -end diff --git a/examples/na_controlling_the_pml/na_controlling_the_pml.ipynb b/examples/na_controlling_the_pml/na_controlling_the_pml.ipynb deleted file mode 100644 index 8fcc910b4..000000000 --- a/examples/na_controlling_the_pml/na_controlling_the_pml.ipynb +++ /dev/null @@ -1,232 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using An Ultrasound Transducer As A Sensor Example\n", - "\n", - "This example shows how an ultrasound transducer can be used as a detector by substituting a transducer object for the normal sensor input structure. It builds on the Defining An Ultrasound Transducer and Simulating Ultrasound Beam Patterns examples.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Notes\n", - "\n", - "The example is based on the original kWave example in Matlab. A few parts were adapted to make it compatible with `k-wave-python`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from copy import deepcopy\n", - "from typing import Dict\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder2D import kspaceFirstOrder2DC\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.colormap import get_color_map\n", - "from kwave.utils.conversion import cart2grid\n", - "from kwave.utils.mapgen import make_cart_circle, make_disc\n", - "from kwave.utils.signals import reorder_binary_sensor_data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create the computational grid and propagation medium\n", - "grid_size = Vector([128, 128]) # [grid points]\n", - "grid_spacing = Vector([0.1e-3, 0.1e-3]) # [m]\n", - "kgrid = kWaveGrid(grid_size, grid_spacing)\n", - "\n", - "medium = kWaveMedium(sound_speed=1500)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create initial pressure distribution using make_disc\n", - "disc_magnitude = 5 # [Pa]\n", - "disc_pos = Vector([50, 50]) # [grid points]\n", - "disc_radius = 8 # [grid points]\n", - "disc_1 = disc_magnitude * make_disc(grid_size, disc_pos, disc_radius)\n", - "\n", - "disc_magnitude = 3 # [Pa]\n", - "disc_pos = Vector([80, 60]) # [grid points]\n", - "disc_radius = 5 # [grid points]\n", - "disc_2 = disc_magnitude * make_disc(grid_size, disc_pos, disc_radius)\n", - "\n", - "source = kSource()\n", - "source.p0 = disc_1 + disc_2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define a centered circular sensor\n", - "sensor_radius = 4e-3 # [m]\n", - "num_sensor_points = 50\n", - "sensor_mask = make_cart_circle(sensor_radius, num_sensor_points)\n", - "sensor = kSensor(sensor_mask)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define a function to run simulation with given options\n", - "def run_simulation_with_options(**simulation_opts) -> Dict[str, np.array]:\n", - " simulation_options = SimulationOptions(\n", - " save_to_disk=True,\n", - " **simulation_opts\n", - " )\n", - " execution_options = SimulationExecutionOptions(is_gpu_simulation=False)\n", - " \n", - " sensor_data = kspaceFirstOrder2DC(\n", - " medium=medium,\n", - " kgrid=kgrid,\n", - " source=deepcopy(source),\n", - " sensor=deepcopy(sensor),\n", - " simulation_options=simulation_options,\n", - " execution_options=execution_options\n", - " )\n", - " return sensor_data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Update the example ID below to change the simulation conditions\n", - "example_idx = 1\n", - "\n", - "if example_idx == 1:\n", - " options = {'pml_alpha': 0}\n", - "elif example_idx == 2:\n", - " options = {'pml_alpha': 1e6}\n", - "elif example_idx == 3:\n", - " options = {'pml_size': 2}\n", - "elif example_idx == 4:\n", - " options = {'pml_inside': False}\n", - "else:\n", - " raise NotImplementedError()\n", - "\n", - "sensor_data = run_simulation_with_options(**options)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "_, _, reorder_index = cart2grid(kgrid, sensor.mask)\n", - "sensor_data_point = reorder_binary_sensor_data(sensor_data['p'].T, reorder_index=reorder_index)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cmap = get_color_map()\n", - "plt.imshow(sensor_data_point, vmin=-1, vmax=1, aspect='auto', cmap=cmap)\n", - "plt.ylabel('Sensor Position')\n", - "plt.xlabel('Time Step')\n", - "plt.colorbar()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Comparing against Matlab implementation\n", - "\n", - "Seems like above example runs successfully but ... How do we know if our results match the Matlab implementation? A simple yet effective strategy is to run Matlab script, record outputs, load them in Python and compare the results. Huh, sounds involved and it is indeed a bit involved. However, we did it and you can do this in two steps:\n", - "1. Open Matlab and run `modified_matlab_example.m` script in this folder.\n", - "2. Execute the cell below.\n", - "\n", - "Step #1 will run the Matlab example and record values. Step #2 will load values and compare `k-wave-python` outputs against Matlab implementation.\n", - "\n", - "You may be wondering at this point, why we do not run `example_na_controlling_the_pml.m` (original Matlab script) instead of the script from this folder. There are two reasons - (1) original example does not record output values in a file and (2) it runs one simulation case at a time. Modified example records output values (recorded pressure and visualized image) and runs all four simulation cases. In case you want to run the original example by adding output recording logic and running all four simulation cases individually, please feel free to do so. If everything is correct, you will get the same results as the `modified_matlab_example.m`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from scipy.io import loadmat\n", - "\n", - "matlab_output_path = f'example_{example_idx}.mat'\n", - "\n", - "if not os.path.exists(matlab_output_path):\n", - " print(\"Data recorded from Matlab script does not exist! Did you run the `modified_matlab_example.m` in Matlab?\")\n", - "else:\n", - " recorded_data = loadmat(matlab_output_path, simplify_cells=True)\n", - " assert np.allclose(recorded_data['sensor_data'].T, sensor_data['p'])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/new_api_ivp_2D.py b/examples/new_api_ivp_2D.py index 46adf6e13..7d0595b8a 100644 --- a/examples/new_api_ivp_2D.py +++ b/examples/new_api_ivp_2D.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # 2D Initial Value Problem (New API) +# A disc-shaped initial pressure propagates outward in a homogeneous medium. + +# %% """ 2D Initial Value Problem using the new unified API. @@ -14,6 +19,7 @@ from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.utils.mapgen import make_disc +# %% Setup and run # Grid grid_size = Vector([128, 128]) grid_spacing = Vector([0.1e-3, 0.1e-3]) diff --git a/examples/new_api_transducer_3D.py b/examples/new_api_transducer_3D.py index 13cdcd97c..1a791d0a5 100644 --- a/examples/new_api_transducer_3D.py +++ b/examples/new_api_transducer_3D.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # 3D Transducer Simulation (New API) +# Demonstrates 3D wave propagation with the unified kspaceFirstOrder() function. + +# %% """ 3D simulation using the new unified API. @@ -14,6 +19,7 @@ from kwave.ksource import kSource from kwave.kspaceFirstOrder import kspaceFirstOrder +# %% Setup and run # 3D grid for demonstration grid_size = Vector([64, 64, 64]) grid_spacing = Vector([0.1e-3, 0.1e-3, 0.1e-3]) diff --git a/examples/pr_2D_FFT_line_sensor/pr_2D_FFT_line_sensor.py b/examples/pr_2D_FFT_line_sensor.py similarity index 79% rename from examples/pr_2D_FFT_line_sensor/pr_2D_FFT_line_sensor.py rename to examples/pr_2D_FFT_line_sensor.py index 59a1b27b0..246e8a999 100644 --- a/examples/pr_2D_FFT_line_sensor/pr_2D_FFT_line_sensor.py +++ b/examples/pr_2D_FFT_line_sensor.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # 2D FFT Reconstruction For A Line Sensor Example +# Reconstruct a 2D photoacoustic wave-field from a linear array using kspaceLineRecon. + +# %% import matplotlib.pyplot as plt import numpy as np from mpl_toolkits.axes_grid1 import make_axes_locatable @@ -8,23 +13,14 @@ from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor from kwave.ksource import kSource -from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.kspaceLineRecon import kspaceLineRecon -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions from kwave.utils.colormap import get_color_map from kwave.utils.filters import smooth from kwave.utils.mapgen import make_disc -# 2D FFT Reconstruction For A Line Sensor Example - -# This example demonstrates the use of k-Wave for the reconstruction of a -# two-dimensional photoacoustic wave-field recorded over a linear array of -# sensor elements The sensor data is simulated using kspaceFirstOrder2D -# and reconstructed using kspaceLineRecon. It builds on the Homogeneous -# Propagation Medium and Heterogeneous Propagation Medium examples. - +# %% def main(): # -------------------- # SIMULATION @@ -32,7 +28,7 @@ def main(): # create the computational grid PML_size = 20 # size of the PML in grid points - N = Vector([128, 256]) - 2 * PML_size # number of grid points + N = Vector([128, 256]) # number of grid points (PML is inside the grid) d = Vector([0.1e-3, 0.1e-3]) # grid point spacing [m] kgrid = kWaveGrid(N, d) @@ -64,17 +60,19 @@ def main(): # create the time array kgrid.makeTime(medium.sound_speed) - # set the input arguments: force the PML to be outside the computational grid - simulation_options = SimulationOptions( - save_to_disk=True, - pml_inside=False, + # %% + # NOTE: pml_inside=False not supported in new API + # run the simulation + sensor_data = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, pml_size=PML_size, smooth_p0=False, + backend="cpp", + device="gpu", ) - execution_options = SimulationExecutionOptions(is_gpu_simulation=True) - - # run the simulation - sensor_data = kspaceFirstOrder2D(kgrid, source, sensor, medium, simulation_options, execution_options) sensor_data = sensor_data["p"] # No transpose needed - fixed in executor # reconstruct the initial pressure @@ -92,6 +90,7 @@ def main(): query_points = np.stack((kgrid.x - kgrid.x.min(), kgrid.y), axis=-1) p_xy_rs = interp_func(query_points) + # %% # -------------------- # VISUALIZATION # -------------------- @@ -101,7 +100,7 @@ def main(): # plot the initial pressure and sensor distribution fig, ax = plt.subplots(1, 1) im = ax.imshow( - p0 + sensor.mask[PML_size:-PML_size, PML_size:-PML_size] * disc_magnitude, + p0 + sensor.mask * disc_magnitude, extent=[kgrid.y_vec.min() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3], vmin=-disc_magnitude, vmax=disc_magnitude, @@ -150,5 +149,6 @@ def main(): plt.show() +# %% if __name__ == "__main__": main() diff --git a/examples/pr_2D_FFT_line_sensor/README.md b/examples/pr_2D_FFT_line_sensor/README.md deleted file mode 100644 index e05c7e151..000000000 --- a/examples/pr_2D_FFT_line_sensor/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 2D FFT Reconstruction For A Line Sensor Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/pr_2D_FFT_line_sensor/pr_2D_FFT_line_sensor.ipynb) - -This example demonstrates the use of k-Wave for the reconstruction of a two-dimensional photoacoustic wave-field recorded over a linear array of sensor elements. The sensor data is simulated using [kspaceFirstOrder2D](https://k-wave-python.readthedocs.io/en/latest/kwave.kspaceFirstOrder2D.html) and reconstructed using kspaceLineRecon. It builds on the Homogeneous Propagation Medium and Heterogeneous Propagation Medium examples. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_pr_2D_fft_line_sensor.php) or its [implementation](https://github.com/ucl-bug/k-wave/blob/main/k-Wave/examples/example_pr_2D_FFT_line_sensor.m). \ No newline at end of file diff --git a/examples/pr_2D_FFT_line_sensor/pr_2D_FFT_line_sensor.ipynb b/examples/pr_2D_FFT_line_sensor/pr_2D_FFT_line_sensor.ipynb deleted file mode 100644 index a65feab0c..000000000 --- a/examples/pr_2D_FFT_line_sensor/pr_2D_FFT_line_sensor.ipynb +++ /dev/null @@ -1,298 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "initial_id", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "48677f5b96e1511e", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from mpl_toolkits.axes_grid1 import make_axes_locatable\n", - "from scipy.interpolate import RegularGridInterpolator\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D\n", - "from kwave.kspaceLineRecon import kspaceLineRecon\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.colormap import get_color_map\n", - "from kwave.utils.filters import smooth\n", - "from kwave.utils.mapgen import make_disc" - ] - }, - { - "cell_type": "markdown", - "id": "262efa49a13b9574", - "metadata": {}, - "source": [ - "## 2D FFT Reconstruction For A Line Sensor Example\n", - "\n", - "This example demonstrates the use of k-Wave for the reconstruction of a\n", - "two-dimensional photoacoustic wave-field recorded over a linear array of\n", - "sensor elements The sensor data is simulated using kspaceFirstOrder2D\n", - "and reconstructed using kspaceLineRecon. It builds on the Homogeneous\n", - "Propagation Medium and Heterogeneous Propagation Medium examples. " - ] - }, - { - "cell_type": "markdown", - "id": "310bcfe8541609c2", - "metadata": {}, - "source": [ - "### SIMULATION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6d7c5328ae5dd3ee", - "metadata": {}, - "outputs": [], - "source": [ - "# create the computational grid\n", - "PML_size = 20 # size of the PML in grid points\n", - "N = Vector([128, 256]) - 2 * PML_size # number of grid points\n", - "d = Vector([0.1e-3, 0.1e-3]) # grid point spacing [m]\n", - "kgrid = kWaveGrid(N, d)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bd5eece140453f0f", - "metadata": {}, - "outputs": [], - "source": [ - "# define the properties of the propagation medium\n", - "medium = kWaveMedium(sound_speed=1500) # [m/s]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2ce3b8059a9ec319", - "metadata": {}, - "outputs": [], - "source": [ - "# create initial pressure distribution using makeDisc\n", - "disc_magnitude = 5 # [Pa]\n", - "disc_pos = Vector([60, 140])\n", - "disc_radius = 5\n", - "disc_2 = disc_magnitude * make_disc(N, disc_pos, disc_radius)\n", - "\n", - "disc_pos = Vector([30, 110])\n", - "disc_radius = 8\n", - "disc_1 = disc_magnitude * make_disc(N, disc_pos, disc_radius)\n", - "\n", - "# smooth the initial pressure distribution and restore the magnitude\n", - "p0 = disc_1 + disc_2\n", - "p0 = smooth(p0, restore_max=True)\n", - "\n", - "source = kSource()\n", - "source.p0 = p0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cf6961e373c6f8e8", - "metadata": {}, - "outputs": [], - "source": [ - "# define a binary line sensor\n", - "sensor = kSensor()\n", - "sensor.mask = np.zeros(N)\n", - "sensor.mask[0] = 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5a278b1e9b253295", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "\n", - "# create the time array\n", - "kgrid.makeTime(medium.sound_speed)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a3802e3e482dc77", - "metadata": {}, - "outputs": [], - "source": [ - "# set the input arguments: force the PML to be outside the computational grid\n", - "simulation_options = SimulationOptions(\n", - " save_to_disk=True,\n", - " pml_inside=False,\n", - " pml_size=PML_size,\n", - " smooth_p0=False,\n", - ")\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "154c372469cd6063", - "metadata": {}, - "outputs": [], - "source": [ - "# run the simulation\n", - "sensor_data = kspaceFirstOrder2D(kgrid, source, sensor, medium, simulation_options, execution_options)\n", - "sensor_data = sensor_data['p'].T\n", - "\n", - "# reconstruct the initial pressure\n", - "p_xy = kspaceLineRecon(sensor_data.T, dy=d[1], dt=kgrid.dt.item(), c=medium.sound_speed.item(),\n", - " pos_cond=True, interp='linear')\n", - "\n", - "# define a second k-space grid using the dimensions of p_xy\n", - "N_recon = Vector(p_xy.shape)\n", - "d_recon = Vector([kgrid.dt.item() * medium.sound_speed.item(), kgrid.dy])\n", - "kgrid_recon = kWaveGrid(N_recon, d_recon)\n", - "\n", - "# resample p_xy to be the same size as source.p0\n", - "interp_func = RegularGridInterpolator(\n", - " (kgrid_recon.x_vec[:, 0] - kgrid_recon.x_vec.min(), kgrid_recon.y_vec[:, 0]),\n", - " p_xy, method='linear'\n", - ")\n", - "query_points = np.stack((kgrid.x - kgrid.x.min(), kgrid.y), axis=-1)\n", - "p_xy_rs = interp_func(query_points)" - ] - }, - { - "cell_type": "markdown", - "id": "b07f65d05feaf902", - "metadata": {}, - "source": [ - "### VISUALIZATION\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "30edd081e39aa21b", - "metadata": {}, - "outputs": [], - "source": [ - "cmap = get_color_map()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d02e73401dee274c", - "metadata": {}, - "outputs": [], - "source": [ - "# plot the initial pressure and sensor distribution\n", - "fig, ax = plt.subplots(1, 1)\n", - "im = ax.imshow(p0 + sensor.mask[PML_size:-PML_size, PML_size:-PML_size] * disc_magnitude,\n", - " extent=[kgrid.y_vec.min() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3],\n", - " vmin=-disc_magnitude, vmax=disc_magnitude, cmap=cmap)\n", - "divider = make_axes_locatable(ax)\n", - "cax = divider.append_axes(\"right\", size=\"3%\", pad=\"2%\")\n", - "ax.set_ylabel('x-position [mm]')\n", - "ax.set_xlabel('y-position [mm]')\n", - "fig.colorbar(im, cax=cax)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc203d6e2a22dec6", - "metadata": {}, - "outputs": [], - "source": [ - "fig, ax = plt.subplots(1, 1)\n", - "im = ax.imshow(sensor_data, vmin=-1, vmax=1, cmap=cmap, aspect='auto')\n", - "divider = make_axes_locatable(ax)\n", - "cax = divider.append_axes(\"right\", size=\"3%\", pad=\"2%\")\n", - "ax.set_ylabel('Sensor Position')\n", - "ax.set_xlabel('Time Step')\n", - "fig.colorbar(im, cax=cax)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2da3a2a311fd1683", - "metadata": {}, - "outputs": [], - "source": [ - "# plot the reconstructed initial pressure\n", - "fig, ax = plt.subplots(1, 1)\n", - "im = ax.imshow(p_xy_rs,\n", - " extent=[kgrid.y_vec.min() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3],\n", - " vmin=-disc_magnitude, vmax=disc_magnitude, cmap=cmap)\n", - "divider = make_axes_locatable(ax)\n", - "cax = divider.append_axes(\"right\", size=\"3%\", pad=\"2%\")\n", - "ax.set_ylabel('x-position [mm]')\n", - "ax.set_xlabel('y-position [mm]')\n", - "fig.colorbar(im, cax=cax)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f7ae5b0a2f5e147e", - "metadata": {}, - "outputs": [], - "source": [ - "# plot a profile for comparison\n", - "plt.plot(kgrid.y_vec[:, 0] * 1e3, p0[disc_pos[0], :], 'k-', label='Initial Pressure')\n", - "plt.plot(kgrid.y_vec[:, 0] * 1e3, p_xy_rs[disc_pos[0], :], 'r--', label='Reconstructed Pressure')\n", - "plt.xlabel('y-position [mm]')\n", - "plt.ylabel('Pressure')\n", - "plt.legend()\n", - "plt.axis('tight')\n", - "plt.ylim([0, 5.1])\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/pr_2D_TR_line_sensor/pr_2D_TR_line_sensor.py b/examples/pr_2D_TR_line_sensor.py similarity index 88% rename from examples/pr_2D_TR_line_sensor/pr_2D_TR_line_sensor.py rename to examples/pr_2D_TR_line_sensor.py index a4a65856d..546e5d5be 100644 --- a/examples/pr_2D_TR_line_sensor/pr_2D_TR_line_sensor.py +++ b/examples/pr_2D_TR_line_sensor.py @@ -1,5 +1,8 @@ -from copy import deepcopy +# %% [markdown] +# # 2D Time Reversal Reconstruction For A Line Sensor Example +# Reconstruct a 2D photoacoustic wave-field using time reversal with a linear array of sensor elements. +# %% import matplotlib.pyplot as plt import numpy as np from mpl_toolkits.axes_grid1 import make_axes_locatable @@ -10,7 +13,8 @@ from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor from kwave.ksource import kSource -from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D +from kwave.kspaceFirstOrder import kspaceFirstOrder +from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D # TimeReversal requires legacy API from kwave.kspaceLineRecon import kspaceLineRecon from kwave.options.simulation_execution_options import SimulationExecutionOptions from kwave.options.simulation_options import SimulationOptions @@ -19,15 +23,8 @@ from kwave.utils.filters import smooth from kwave.utils.mapgen import make_disc -# 2D Time Reversal Reconstruction For A Line Sensor Example - -# This example demonstrates the use of k-Wave for the time-reversal -# reconstruction of a two-dimensional photoacoustic wave-field recorded -# over a linear array of sensor elements. The sensor data is simulated and -# then time-reversed using kspaceFirstOrder2D. It builds on the 2D FFT -# Reconstruction For A Line Sensor Example. - +# %% def main(): # -------------------- # SIMULATION @@ -71,25 +68,35 @@ def main(): inner_mask[0, :] = 1 # Line sensor along the first row sensor.mask = inner_mask sensor.record = ["p", "p_final"] - # set the input arguments: force the PML to be outside the computational - # grid; switch off p0 smoothing within kspaceFirstOrder2D - simulation_options = SimulationOptions( - pml_inside=False, + + # %% + # NOTE: pml_inside=False, data_cast="single" not supported in new API + # run the simulation + sensor_data = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, pml_size=PML_size, smooth_p0=False, - save_to_disk=True, - data_cast="single", + backend="cpp", + device="gpu", ) - execution_options = SimulationExecutionOptions(is_gpu_simulation=True) - - # run the simulation - sensor_data = kspaceFirstOrder2D(kgrid, source, deepcopy(sensor), medium, simulation_options, execution_options) sensor.recorded_pressure = sensor_data["p"].T # Store the recorded pressure data # reset only the initial pressure, keep the sensor with recorded data source = kSource() # create time reversal handler and run reconstruction tr = TimeReversal(kgrid, medium, sensor) + # NOTE: TimeReversal requires the legacy API until it is updated + simulation_options = SimulationOptions( + pml_inside=False, + pml_size=PML_size, + smooth_p0=False, + save_to_disk=True, + data_cast="single", + ) + execution_options = SimulationExecutionOptions(is_gpu_simulation=True) p0_recon = tr(kspaceFirstOrder2D, simulation_options, execution_options) # repeat the FFT reconstruction for comparison @@ -107,6 +114,7 @@ def main(): query_points = np.stack((kgrid.x - kgrid.x.min(), kgrid.y), axis=-1) p_xy_rs = interp_func(query_points) + # %% # -------------------- # VISUALIZATION # -------------------- @@ -178,5 +186,6 @@ def main(): plt.show() +# %% if __name__ == "__main__": main() diff --git a/examples/pr_2D_TR_line_sensor/README.md b/examples/pr_2D_TR_line_sensor/README.md deleted file mode 100644 index 2c5d15c98..000000000 --- a/examples/pr_2D_TR_line_sensor/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 2D Time Reversal Reconstruction For A Line Sensor Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/pr_2D_TR_line_sensor/pr_2D_TR_line_sensor.ipynb) - -This example demonstrates the use of k-Wave for the time-reversal reconstruction of a two-dimensional photoacoustic wave-field recorded over a linear array of sensor elements. The sensor data is simulated and then time-reversed using [kspaceFirstOrder2D](https://k-wave-python.readthedocs.io/en/latest/kwave.kspaceFirstOrder2D.html). It builds on the [2D FFT Reconstruction For A Line Sensor Example](../pr_2D_TR_line_sensor/). - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_pr_2D_tr_line_sensor.php) or its [implementation](https://github.com/ucl-bug/k-wave/blob/main/k-Wave/examples/example_pr_2D_TR_line_sensor.m). \ No newline at end of file diff --git a/examples/pr_2D_TR_line_sensor/pr_2D_TR_line_sensor.ipynb b/examples/pr_2D_TR_line_sensor/pr_2D_TR_line_sensor.ipynb deleted file mode 100644 index bd4e1d0cb..000000000 --- a/examples/pr_2D_TR_line_sensor/pr_2D_TR_line_sensor.ipynb +++ /dev/null @@ -1,335 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "initial_id", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4d18dbe95c4a69b0", - "metadata": {}, - "outputs": [], - "source": [ - "from copy import deepcopy\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from mpl_toolkits.axes_grid1 import make_axes_locatable\n", - "from scipy.interpolate import RegularGridInterpolator\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D\n", - "from kwave.kspaceLineRecon import kspaceLineRecon\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.reconstruction.time_reversal import TimeReversal\n", - "from kwave.utils.colormap import get_color_map\n", - "from kwave.utils.filters import smooth\n", - "from kwave.utils.mapgen import make_disc" - ] - }, - { - "cell_type": "markdown", - "id": "88cb3b08164514b0", - "metadata": {}, - "source": [ - "## 2D Time Reversal Reconstruction For A Line Sensor Example\n", - "\n", - "This example demonstrates the use of k-Wave for the time-reversal\n", - "reconstruction of a two-dimensional photoacoustic wave-field recorded\n", - "over a linear array of sensor elements. The sensor data is simulated and\n", - "then time-reversed using kspaceFirstOrder2D. It builds on the 2D FFT \n", - "Reconstruction For A Line Sensor Example." - ] - }, - { - "cell_type": "markdown", - "id": "76db584c54e13248", - "metadata": {}, - "source": [ - "### SIMULATION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6eeb7e260b43f43", - "metadata": {}, - "outputs": [], - "source": [ - "# create the computational grid\n", - "PML_size = 20 # size of the PML in grid points\n", - "N = Vector([128, 256]) - 2 * PML_size # number of grid points\n", - "d = Vector([0.1e-3, 0.1e-3]) # grid point spacing [m]\n", - "kgrid = kWaveGrid(N, d)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7f72a8c783169161", - "metadata": {}, - "outputs": [], - "source": [ - "# define the properties of the propagation medium\n", - "medium = kWaveMedium(\n", - " sound_speed=1500,\t# [m/s]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "619c6b4d5a08c7a5", - "metadata": {}, - "outputs": [], - "source": [ - "# create initial pressure distribution using makeDisc\n", - "disc_magnitude = 5 # [Pa]\n", - "disc_pos = Vector([60, 140]) # [grid points]\n", - "disc_radius = 5 # [grid points]\n", - "disc_2 = disc_magnitude * make_disc(N, disc_pos, disc_radius)\n", - "\n", - "disc_pos = Vector([30, 110]) # [grid points]\n", - "disc_radius = 8 # [grid points]\n", - "disc_1 = disc_magnitude * make_disc(N, disc_pos, disc_radius)\n", - "\n", - "# smooth the initial pressure distribution and restore the magnitude\n", - "p0 = smooth(disc_1 + disc_2, restore_max=True)\n", - "\n", - "# assign to the source structure\n", - "source = kSource()\n", - "source.p0 = p0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "59bbb876871f155e", - "metadata": {}, - "outputs": [], - "source": [ - "# define a binary line sensor\n", - "sensor = kSensor()\n", - "sensor.mask = np.zeros(N)\n", - "sensor.mask[0] = 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a23b0c47baf1bde2", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "\n", - "# create the time array\n", - "kgrid.makeTime(medium.sound_speed)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c85b2aacc4cc6bcc", - "metadata": {}, - "outputs": [], - "source": [ - "# set the input arguments: force the PML to be outside the computational\n", - "# grid; switch off p0 smoothing within kspaceFirstOrder2D\n", - "simulation_options = SimulationOptions(\n", - " pml_inside=False,\n", - " pml_size=PML_size,\n", - " smooth_p0=False,\n", - " save_to_disk=True,\n", - " data_cast=\"single\",\n", - ")\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "db85c9943ed5f91a", - "metadata": {}, - "outputs": [], - "source": [ - "# run the simulation\n", - "sensor_data = kspaceFirstOrder2D(kgrid, source, deepcopy(sensor), medium, simulation_options, execution_options)\n", - "sensor_data = sensor_data['p'].T\n", - "sensor.recorded_pressure = sensor_data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc22514bed1e2a98", - "metadata": {}, - "outputs": [], - "source": [ - "# remove padding from sensor mask that was added by kspaceFirstOrder2D\n", - "source = kSource()\n", - "\n", - "# create time reversal handler and run reconstruction\n", - "tr = TimeReversal(kgrid, medium, sensor)\n", - "p0_recon = tr(kspaceFirstOrder2D, simulation_options, execution_options)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "431b7bb0ee525b5e", - "metadata": {}, - "outputs": [], - "source": [ - "# repeat the FFT reconstruction for comparison\n", - "p_xy = kspaceLineRecon(sensor_data.T, dy=d[1], dt=kgrid.dt.item(), c=medium.sound_speed.item(), pos_cond=True, interp='linear')\n", - "\n", - "# define a second k-space grid using the dimensions of p_xy\n", - "N_recon = Vector(p_xy.shape)\n", - "d_recon = Vector([kgrid.dt * medium.sound_speed.item(), kgrid.dy])\n", - "kgrid_recon = kWaveGrid(N_recon, d_recon)\n", - "\n", - "# resample p_xy to be the same size as source.p0\n", - "interp_func = RegularGridInterpolator(\n", - " (kgrid_recon.x_vec[:, 0] - kgrid_recon.x_vec.min(), kgrid_recon.y_vec[:, 0]),\n", - " p_xy, method='linear'\n", - ")\n", - "query_points = np.stack((kgrid.x - kgrid.x.min(), kgrid.y), axis=-1)\n", - "p_xy_rs = interp_func(query_points)" - ] - }, - { - "cell_type": "markdown", - "id": "7305f8eba3374df2", - "metadata": {}, - "source": [ - "### VISUALIZATION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7c8e399f00971c60", - "metadata": {}, - "outputs": [], - "source": [ - "cmap = get_color_map()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b159649563e8767e", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot the initial pressure and sensor distribution\n", - "fig, ax = plt.subplots(1, 1)\n", - "im = ax.imshow(p0 + sensor.mask * disc_magnitude,\n", - " extent=[kgrid.y_vec.min() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3],\n", - " vmin=-disc_magnitude, vmax=disc_magnitude, cmap=cmap)\n", - "divider = make_axes_locatable(ax)\n", - "cax = divider.append_axes(\"right\", size=\"3%\", pad=\"2%\")\n", - "ax.set_ylabel('x-position [mm]')\n", - "ax.set_xlabel('y-position [mm]')\n", - "fig.colorbar(im, cax=cax)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9bc5d6d4b6761d4", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot the reconstructed initial pressure\n", - "fig, ax = plt.subplots(1, 1)\n", - "im = ax.imshow(p0_recon,\n", - " extent=[kgrid.y_vec.min() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3],\n", - " vmin=-disc_magnitude, vmax=disc_magnitude, cmap=cmap)\n", - "divider = make_axes_locatable(ax)\n", - "cax = divider.append_axes(\"right\", size=\"3%\", pad=\"2%\")\n", - "ax.set_ylabel('x-position [mm]')\n", - "ax.set_xlabel('y-position [mm]')\n", - "fig.colorbar(im, cax=cax)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4b70ab1746f0ed88", - "metadata": {}, - "outputs": [], - "source": [ - "# Apply a positivity condition\n", - "p0_recon[p0_recon < 0] = 0\n", - "\n", - "# Plot the reconstructed initial pressure with positivity condition\n", - "fig, ax = plt.subplots(1, 1)\n", - "im = ax.imshow(p0_recon,\n", - " extent=[kgrid.y_vec.min() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3],\n", - " vmin=-disc_magnitude, vmax=disc_magnitude, cmap=cmap)\n", - "divider = make_axes_locatable(ax)\n", - "cax = divider.append_axes(\"right\", size=\"3%\", pad=\"2%\")\n", - "ax.set_ylabel('x-position [mm]')\n", - "ax.set_xlabel('y-position [mm]')\n", - "fig.colorbar(im, cax=cax)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a95d4b11c4111e59", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot a profile for comparison\n", - "plt.plot(kgrid.y_vec[:, 0] * 1e3, p0[disc_pos[0], :], 'k-', label='Initial Pressure')\n", - "plt.plot(kgrid.y_vec[:, 0] * 1e3, p_xy_rs[disc_pos[0], :], 'r--', label='FFT Reconstruction')\n", - "plt.plot(kgrid.y_vec[:, 0] * 1e3, p0_recon[disc_pos[0], :], 'b:', label='Time Reversal')\n", - "plt.xlabel('y-position [mm]')\n", - "plt.ylabel('Pressure')\n", - "plt.legend()\n", - "plt.axis('tight')\n", - "plt.ylim([0, 5.1])\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/pr_3D_FFT_planar_sensor/pr_3D_FFT_planar_sensor.py b/examples/pr_3D_FFT_planar_sensor.py similarity index 83% rename from examples/pr_3D_FFT_planar_sensor/pr_3D_FFT_planar_sensor.py rename to examples/pr_3D_FFT_planar_sensor.py index ea7114494..115412697 100644 --- a/examples/pr_3D_FFT_planar_sensor/pr_3D_FFT_planar_sensor.py +++ b/examples/pr_3D_FFT_planar_sensor.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # 3D FFT Reconstruction For A Planar Sensor Example +# Reconstruct a 3D photoacoustic wave-field from a planar sensor array using kspacePlaneRecon. + +# %% import matplotlib.pyplot as plt import numpy as np from scipy.interpolate import RegularGridInterpolator @@ -7,23 +12,14 @@ from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor from kwave.ksource import kSource -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.kspacePlaneRecon import kspacePlaneRecon -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions from kwave.utils.colormap import get_color_map from kwave.utils.filters import smooth from kwave.utils.mapgen import make_ball -# 3D FFT Reconstruction For A Planar Sensor Example - -# This example demonstrates the use of k-Wave for the reconstruction of a -# three-dimensional photoacoustic wave-field recorded over a planar array -# of sensor elements. The sensor data is simulated using kspaceFirstOrder3D -# and reconstructed using kspacePlaneRecon. It builds on the Simulations In -# Three Dimensions and 2D FFT Reconstruction For A Line Sensor examples. - +# %% def main(): # -------------------- # SIMULATION @@ -33,13 +29,16 @@ def main(): # create the computational grid PML_size = 10 # size of the PML in grid points - N = Vector([32, 64, 64]) * scale - 2 * PML_size # number of grid points + N = Vector([32, 64, 64]) * scale # number of grid points (PML is inside the grid) d = Vector([0.2e-3, 0.2e-3, 0.2e-3]) / scale # grid point spacing [m] kgrid = kWaveGrid(N, d) # define the properties of the propagation medium medium = kWaveMedium(sound_speed=1500) # [m/s] + # create the time array + kgrid.makeTime(medium.sound_speed) + # create initial pressure distribution using makeBall ball_magnitude = 10 # [Pa] ball_radius = 3 * scale # [grid points] @@ -55,14 +54,20 @@ def main(): sensor_mask[0] = 1 sensor.mask = sensor_mask - # set the input arguments - simulation_options = SimulationOptions(save_to_disk=True, pml_size=PML_size, pml_inside=False, smooth_p0=False, data_cast="single") - - execution_options = SimulationExecutionOptions(is_gpu_simulation=True) - + # %% + # NOTE: pml_inside=False, data_cast="single" not supported in new API # run the simulation - sensor_data = kspaceFirstOrder3D(kgrid, source, sensor, medium, simulation_options, execution_options) - sensor_data = sensor_data["p"].T + sensor_data = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + pml_size=PML_size, + smooth_p0=False, + backend="cpp", + device="gpu", + ) + sensor_data = sensor_data["p"] # reshape sensor data to y, z, t sensor_data_rs = sensor_data.reshape(N[1], N[2], kgrid.Nt) @@ -95,6 +100,7 @@ def main(): ) p_xyz_rs = interp_func(query_points) + # %% # -------------------- # VISUALIZATION # -------------------- @@ -201,5 +207,6 @@ def main(): plt.show() +# %% if __name__ == "__main__": main() diff --git a/examples/pr_3D_FFT_planar_sensor/README.md b/examples/pr_3D_FFT_planar_sensor/README.md deleted file mode 100644 index a140626cf..000000000 --- a/examples/pr_3D_FFT_planar_sensor/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 3D FFT Reconstruction For A Planar Sensor Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/pr_3D_FFT_planar_sensor/pr_3D_FFT_planar_sensor.ipynb) - -This example demonstrates the use of k-Wave for the reconstruction of a three-dimensional photoacoustic wave-field recorded over a planar array of sensor elements. The sensor data is simulated using [kspaceFirstOrder3D](https://k-wave-python.readthedocs.io/en/latest/kwave.kspaceFirstOrder3D.html) and reconstructed using kspacePlaneRecon. It builds on the Simulations In Three Dimensions and [2D FFT Reconstruction For A Line Sensor](../pr_3D_FFT_planar_sensor/) examples. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_pr_3D_fft_planar_sensor.php) or its [implementation](https://github.com/ucl-bug/k-wave/blob/main/k-Wave/examples/example_pr_3D_FFT_planar_sensor.m). \ No newline at end of file diff --git a/examples/pr_3D_FFT_planar_sensor/pr_3D_FFT_planar_sensor.ipynb b/examples/pr_3D_FFT_planar_sensor/pr_3D_FFT_planar_sensor.ipynb deleted file mode 100644 index fc4efd30e..000000000 --- a/examples/pr_3D_FFT_planar_sensor/pr_3D_FFT_planar_sensor.ipynb +++ /dev/null @@ -1,313 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "initial_id", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "611488037b162a21", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from scipy.interpolate import RegularGridInterpolator\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.kspacePlaneRecon import kspacePlaneRecon\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.colormap import get_color_map\n", - "from kwave.utils.filters import smooth\n", - "from kwave.utils.mapgen import make_ball" - ] - }, - { - "cell_type": "markdown", - "id": "d7d832f9f7f7caf3", - "metadata": {}, - "source": [ - "## 3D FFT Reconstruction For A Planar Sensor Example\n", - "\n", - "This example demonstrates the use of k-Wave for the reconstruction of a\n", - "three-dimensional photoacoustic wave-field recorded over a planar array\n", - "of sensor elements. The sensor data is simulated using kspaceFirstOrder3D\n", - "and reconstructed using kspacePlaneRecon. It builds on the Simulations In\n", - "Three Dimensions and 2D FFT Reconstruction For A Line Sensor examples." - ] - }, - { - "cell_type": "markdown", - "id": "6e5153f4bb9a8e8b", - "metadata": {}, - "source": [ - "### SIMULATION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5ef3d745e587ea7d", - "metadata": {}, - "outputs": [], - "source": [ - "scale = 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "62a1b78d509423b1", - "metadata": {}, - "outputs": [], - "source": [ - "# create the computational grid\n", - "PML_size = 10 # size of the PML in grid points\n", - "N = Vector([32, 64, 64]) * scale - 2 * PML_size # number of grid points\n", - "d = Vector([0.2e-3, 0.2e-3, 0.2e-3]) / scale # grid point spacing [m]\n", - "kgrid = kWaveGrid(N, d)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9606aea53baa8a1", - "metadata": {}, - "outputs": [], - "source": [ - "# define the properties of the propagation medium\n", - "medium = kWaveMedium(sound_speed=1500) # [m/s]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5ab847f521ec9b9", - "metadata": {}, - "outputs": [], - "source": [ - "# create initial pressure distribution using makeBall\n", - "ball_magnitude = 10 # [Pa]\n", - "ball_radius = 3 * scale # [grid points]\n", - "p0 = ball_magnitude * make_ball(N, N / 2, ball_radius)\n", - "p0 = smooth(p0, restore_max=True)\n", - "\n", - "source = kSource()\n", - "source.p0 = p0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "80a91cfee6c4b79f", - "metadata": {}, - "outputs": [], - "source": [ - "# define a binary planar sensor\n", - "sensor = kSensor()\n", - "sensor_mask = np.zeros(N)\n", - "sensor_mask[0] = 1\n", - "sensor.mask = sensor_mask" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "39e4252f2d55caae", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "\n", - "# create the time array\n", - "kgrid.makeTime(medium.sound_speed)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d036c62f78598f2d", - "metadata": {}, - "outputs": [], - "source": [ - "# set the input arguments\n", - "simulation_options = SimulationOptions(\n", - " save_to_disk=True,\n", - " pml_size=PML_size,\n", - " pml_inside=False,\n", - " smooth_p0=False,\n", - " data_cast='single'\n", - ")\n", - "\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e74cd39b11526d81", - "metadata": {}, - "outputs": [], - "source": [ - "# run the simulation\n", - "sensor_data = kspaceFirstOrder3D(kgrid, source, sensor, medium, simulation_options, execution_options)\n", - "sensor_data = sensor_data['p'].T\n", - "\n", - "# reshape sensor data to y, z, t\n", - "sensor_data_rs = sensor_data.reshape(N[1], N[2], kgrid.Nt)\n", - "\n", - "# reconstruct the initial pressure\n", - "p_xyz = kspacePlaneRecon(sensor_data_rs, kgrid.dy, kgrid.dz, kgrid.dt.item(),\n", - " medium.sound_speed.item(), data_order='yzt', pos_cond=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c0d05511c962ede8", - "metadata": {}, - "outputs": [], - "source": [ - "# define a k-space grid using the dimensions of p_xyz\n", - "N_recon = Vector(p_xyz.shape)\n", - "d_recon = Vector([kgrid.dt.item() * medium.sound_speed.item(), kgrid.dy, kgrid.dz])\n", - "kgrid_recon = kWaveGrid(N_recon, d_recon)\n", - "\n", - "# define a k-space grid with the same z-spacing as p0\n", - "kgrid_interp = kWaveGrid(N, d)\n", - "\n", - "# resample the p_xyz to be the same size as p0\n", - "interp_func = RegularGridInterpolator(\n", - " (kgrid_recon.x_vec[:, 0] - kgrid_recon.x_vec[:, 0].min(),\n", - " kgrid_recon.y_vec[:, 0] - kgrid_recon.y_vec[:, 0].min(),\n", - " kgrid_recon.z_vec[:, 0] - kgrid_recon.z_vec[:, 0].min()),\n", - " p_xyz, method='linear'\n", - ")\n", - "query_points = np.stack((kgrid_interp.x - kgrid_interp.x.min(),\n", - " kgrid_interp.y - kgrid_interp.y.min(),\n", - " kgrid_interp.z - kgrid_interp.z.min()),\n", - " axis=-1)\n", - "p_xyz_rs = interp_func(query_points)" - ] - }, - { - "cell_type": "markdown", - "id": "c63150dfb5bc2f2f", - "metadata": {}, - "source": [ - "### VISUALIZATION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d612fbaa36e986a9", - "metadata": {}, - "outputs": [], - "source": [ - "# plot the initial pressure and sensor surface in voxel form\n", - "# from kwave.utils.plot import voxel_plot\n", - "# voxel_plot(np.single((p0 + sensor_mask) > 0)) # todo: needs unsmoothed po + plot not working" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2db2c76fa8f0b234", - "metadata": {}, - "outputs": [], - "source": [ - "# plot the initial pressure\n", - "plot_scale = [-10, 10]\n", - "fig, axs = plt.subplots(2, 2)\n", - "axs[0, 0].imshow(p0[:, :, N[2] // 2],\n", - " extent=[kgrid_interp.y_vec.min() * 1e3, kgrid_interp.y_vec.max() * 1e3,\n", - " kgrid_interp.x_vec.max() * 1e3, kgrid_interp.x_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=get_color_map())\n", - "axs[0, 0].set_title('x-y plane')\n", - "\n", - "axs[0, 1].imshow(p0[:, N[1] // 2, :],\n", - " extent=[kgrid_interp.z_vec.min() * 1e3, kgrid_interp.z_vec.max() * 1e3,\n", - " kgrid_interp.x_vec.max() * 1e3, kgrid_interp.x_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=get_color_map())\n", - "axs[0, 1].set_title('x-z plane')\n", - "\n", - "axs[1, 0].imshow(p0[N[0] // 2, :, :],\n", - " extent=[kgrid_interp.z_vec.min() * 1e3, kgrid_interp.z_vec.max() * 1e3,\n", - " kgrid_interp.y_vec.max() * 1e3, kgrid_interp.y_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=get_color_map())\n", - "axs[1, 0].set_title('y-z plane')\n", - "\n", - "axs[1, 1].axis('off')\n", - "axs[1, 1].set_title('(All axes in mm)')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d5f54008217d6532", - "metadata": {}, - "outputs": [], - "source": [ - "# plot the reconstructed initial pressure\n", - "fig, axs = plt.subplots(2, 2)\n", - "axs[0, 0].imshow(p_xyz_rs[:, :, N[2] // 2],\n", - " extent=[kgrid_interp.y_vec.min() * 1e3, kgrid_interp.y_vec.max() * 1e3,\n", - " kgrid_interp.x_vec.max() * 1e3, kgrid_interp.x_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=get_color_map())\n", - "axs[0, 0].set_title('x-y plane')\n", - "\n", - "axs[0, 1].imshow(p_xyz_rs[:, N[1] // 2, :],\n", - " extent=[kgrid_interp.z_vec.min() * 1e3, kgrid_interp.z_vec.max() * 1e3,\n", - " kgrid_interp.x_vec.max() * 1e3, kgrid_interp.x_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=get_color_map())\n", - "axs[0, 1].set_title('x-z plane')\n", - "\n", - "axs[1, 0].imshow(p_xyz_rs[N[0] // 2, :, :],\n", - " extent=[kgrid_interp.z_vec.min() * 1e3, kgrid_interp.z_vec.max() * 1e3,\n", - " kgrid_interp.y_vec.max() * 1e3, kgrid_interp.y_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=get_color_map())\n", - "axs[1, 0].set_title('y-z plane')\n", - "\n", - "axs[1, 1].axis('off')\n", - "axs[1, 1].set_title('(All axes in mm)')\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/pr_3D_TR_planar_sensor/pr_3D_TR_planar_sensor.py b/examples/pr_3D_TR_planar_sensor.py similarity index 90% rename from examples/pr_3D_TR_planar_sensor/pr_3D_TR_planar_sensor.py rename to examples/pr_3D_TR_planar_sensor.py index 7c99efe2b..b148c29e7 100644 --- a/examples/pr_3D_TR_planar_sensor/pr_3D_TR_planar_sensor.py +++ b/examples/pr_3D_TR_planar_sensor.py @@ -1,5 +1,8 @@ -from copy import deepcopy +# %% [markdown] +# # 3D Time Reversal Reconstruction For A Planar Sensor Example +# Reconstruct a 3D photoacoustic wave-field using time reversal with a planar sensor array. +# %% import matplotlib.pyplot as plt import numpy as np from mpl_toolkits.axes_grid1 import make_axes_locatable @@ -9,7 +12,8 @@ from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor from kwave.ksource import kSource -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D +from kwave.kspaceFirstOrder import kspaceFirstOrder +from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D # TimeReversal requires legacy API from kwave.options.simulation_execution_options import SimulationExecutionOptions from kwave.options.simulation_options import SimulationOptions from kwave.reconstruction import TimeReversal @@ -17,16 +21,8 @@ from kwave.utils.filters import smooth from kwave.utils.mapgen import make_ball -# 3D Time Reversal Reconstruction For A Planar Sensor Example - -# This example demonstrates the use of k-Wave for the reconstruction -# of a three-dimensional photoacoustic wave-field recorded over a planar -# array of sensor elements. The sensor data is simulated and then -# time-reversed using kspaceFirstOrder3D. It builds on the 3D FFT -# Reconstruction For A Planar Sensor and 2D Time Reversal Reconstruction -# For A Line Sensor examples. - +# %% def main(): # -------------------- # SIMULATION @@ -67,18 +63,19 @@ def main(): sensor.mask[0, :, :] = 1 # Planar sensor along the first x-plane sensor.record = ["p", "p_final"] - # set the input arguments - simulation_options = SimulationOptions( - pml_inside=False, + # %% + # NOTE: pml_inside=False, data_cast="single" not supported in new API + # run the simulation + sensor_data = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, pml_size=PML_size, smooth_p0=False, - save_to_disk=True, - data_cast="single", + backend="cpp", + device="gpu", ) - execution_options = SimulationExecutionOptions(is_gpu_simulation=True) - - # run the simulation - sensor_data = kspaceFirstOrder3D(kgrid, source, deepcopy(sensor), medium, simulation_options, execution_options) sensor.recorded_pressure = sensor_data["p"].T # Store the recorded pressure data # reset only the initial pressure source @@ -86,8 +83,18 @@ def main(): # create time reversal handler and run reconstruction tr = TimeReversal(kgrid, medium, sensor) + # NOTE: TimeReversal requires the legacy API until it is updated + simulation_options = SimulationOptions( + pml_inside=False, + pml_size=PML_size, + smooth_p0=False, + save_to_disk=True, + data_cast="single", + ) + execution_options = SimulationExecutionOptions(is_gpu_simulation=True) p0_recon = tr(kspaceFirstOrder3D, simulation_options, execution_options) + # %% # -------------------- # VISUALIZATION # -------------------- @@ -210,5 +217,6 @@ def main(): plt.show() +# %% if __name__ == "__main__": main() diff --git a/examples/pr_3D_TR_planar_sensor/README.md b/examples/pr_3D_TR_planar_sensor/README.md deleted file mode 100644 index 6935494fd..000000000 --- a/examples/pr_3D_TR_planar_sensor/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# 3D Time Reversal Reconstruction For A Planar Sensor Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/pr_3D_TR_planar_sensor/pr_3D_TR_planar_sensor.ipynb) - -This example demonstrates the use of k-Wave for the reconstruction of a three-dimensional photoacoustic wave-field recorded over a planar array of sensor elements. The sensor data is simulated and then time-reversed using [kspaceFirstOrder3D](https://k-wave-python.readthedocs.io/en/latest/kwave.kspaceFirstOrder3D.html). It builds on the [3D FFT Reconstruction For A Planar Sensor](../pr_3D_FFT_planar_sensor/) and [2D Time Reversal Reconstruction For A Line Sensor](../pr_2D_TR_line_sensor) examples. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_pr_3D_tr_planar_sensor.php) or its [implementation](https://github.com/ucl-bug/k-wave/blob/main/k-Wave/examples/example_pr_3D_TR_planar_sensor.m). \ No newline at end of file diff --git a/examples/pr_3D_TR_planar_sensor/pr_3D_TR_planar_sensor.ipynb b/examples/pr_3D_TR_planar_sensor/pr_3D_TR_planar_sensor.ipynb deleted file mode 100644 index 38a8684ec..000000000 --- a/examples/pr_3D_TR_planar_sensor/pr_3D_TR_planar_sensor.ipynb +++ /dev/null @@ -1,307 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "initial_id", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6a6cdd4e64365d3e", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.colormap import get_color_map\n", - "from kwave.utils.filters import smooth\n", - "from kwave.utils.mapgen import make_ball" - ] - }, - { - "cell_type": "markdown", - "id": "e7d1f7d7d6481a2c", - "metadata": {}, - "source": [ - "## 3D Time Reversal Reconstruction For A Planar Sensor Example\n", - "\n", - "This example demonstrates the use of k-Wave for the reconstruction of a three-dimensional photoacoustic wave-field recorded over a planar array of sensor elements. The sensor data is simulated and then time-reversed using kspaceFirstOrder3D. It builds on the 3D FFT Reconstruction For A Planar Sensor and 2D Time Reversal Reconstruction For A Line Sensor examples." - ] - }, - { - "cell_type": "markdown", - "id": "4a805d6488afc4b7", - "metadata": {}, - "source": [ - "### SIMULATION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b8f9b1f3d5bef716", - "metadata": {}, - "outputs": [], - "source": [ - "# change scale to 2 to reproduce the higher resolution figures used in the\n", - "# help file\n", - "scale = 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "249a1bd033f3abe7", - "metadata": {}, - "outputs": [], - "source": [ - "# create the computational grid\n", - "PML_size = 10 # size of the PML in grid points\n", - "N = Vector([32, 64, 64]) * scale - 2 * PML_size # number of grid points\n", - "d = Vector([0.2e-3, 0.2e-3, 0.2e-3]) / scale # grid point spacing [m]\n", - "kgrid = kWaveGrid(N, d)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25d647c865e569b", - "metadata": {}, - "outputs": [], - "source": [ - "# define the properties of the propagation medium\n", - "medium = kWaveMedium(sound_speed=1500) # [m/s]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cb75ff29974130cf", - "metadata": {}, - "outputs": [], - "source": [ - "# create initial pressure distribution using makeBall\n", - "ball_magnitude = 10 # [Pa]\n", - "ball_radius = 3 * scale # [grid points]\n", - "p0 = ball_magnitude * make_ball(N, N / 2, ball_radius)\n", - "\n", - "# smooth the initial pressure distribution and restore the magnitude\n", - "p0 = smooth(p0, True)\n", - "\n", - "# assign to the source structure\n", - "source = kSource()\n", - "source.p0 = p0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9eedddfe4204cea5", - "metadata": {}, - "outputs": [], - "source": [ - "# define a binary planar sensor\n", - "sensor = kSensor()\n", - "sensor.mask = np.zeros(N, dtype=bool)\n", - "sensor.mask[0, :, :] = 1\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7ea0924151a96a6f", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "\n", - "# create the time array\n", - "kgrid.makeTime(medium.sound_speed)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7ad7415e71716b05", - "metadata": {}, - "outputs": [], - "source": [ - "# set the input arguments\n", - "simulation_options = SimulationOptions(\n", - " pml_inside=False,\n", - " pml_size=PML_size,\n", - " smooth_p0=False,\n", - " save_to_disk=True,\n", - " data_cast=\"single\",\n", - ")\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True)\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "192d9eeefd209077", - "metadata": {}, - "outputs": [], - "source": [ - "# run the simulation\n", - "from copy import deepcopy\n", - "\n", - "sensor_data = kspaceFirstOrder3D(kgrid, source, deepcopy(sensor), medium, simulation_options, execution_options)\n", - "sensor_data= sensor_data[\"p\"].T" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "790676732e5f078c", - "metadata": {}, - "outputs": [], - "source": [ - "# reset the initial pressure\n", - "from kwave.reconstruction.time_reversal import TimeReversal\n", - "\n", - "source = kSource()\n", - "sensor = kSensor()\n", - "sensor.mask = np.zeros(N)\n", - "sensor.mask[0] = 1\n", - "\n", - "# assign the time reversal data\n", - "sensor.recorded_pressure = sensor_data\n", - "\n", - "# run the time-reversal reconstruction\n", - "tr = TimeReversal(kgrid, medium, sensor)\n", - "p0_recon = tr(kspaceFirstOrder3D, simulation_options, execution_options)\n" - ] - }, - { - "cell_type": "markdown", - "id": "7bc3a5e3b2f4571", - "metadata": {}, - "source": [ - "### VISUALIZATION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7f16ca4a29e4b671", - "metadata": {}, - "outputs": [], - "source": [ - "cmap = get_color_map()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ac432a6b8e284bda", - "metadata": {}, - "outputs": [], - "source": [ - "plot_scale = [-10, 10]\n", - "\n", - "# plot the initial pressure\n", - "fig, axs = plt.subplots(2, 2)\n", - "axs[0, 0].imshow(p0[:, :, N[2] // 2],\n", - " extent=[kgrid.y_vec.min() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=cmap)\n", - "axs[0, 0].set_title('x-y plane')\n", - "axs[0, 0].axis('image')\n", - "\n", - "axs[0, 1].imshow(p0[:, N[1] // 2, :],\n", - " extent=[kgrid.z_vec.min() * 1e3, kgrid.z_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=cmap)\n", - "axs[0, 1].set_title('x-z plane')\n", - "axs[0, 1].axis('image')\n", - "\n", - "axs[1, 0].imshow(p0[N[0] // 2, :, :],\n", - " extent=[kgrid.z_vec.min() * 1e3, kgrid.z_vec.max() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.y_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=cmap)\n", - "axs[1, 0].set_title('y-z plane')\n", - "axs[1, 0].axis('image')\n", - "\n", - "axs[1, 1].axis('off')\n", - "axs[1, 1].set_title('(All axes in mm)')\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "8616da04", - "metadata": {}, - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e946817f00b55e9e", - "metadata": {}, - "outputs": [], - "source": [ - "# plot the reconstructed initial pressure\n", - "fig, axs = plt.subplots(2, 2)\n", - "axs[0, 0].imshow(p0_recon[:, :, N[2] // 2],\n", - " extent=[kgrid.y_vec.min() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=cmap)\n", - "axs[0, 0].set_title('x-y plane')\n", - "axs[0, 0].axis('image')\n", - "\n", - "axs[0, 1].imshow(p0_recon[:, N[1] // 2, :],\n", - " extent=[kgrid.z_vec.min() * 1e3, kgrid.z_vec.max() * 1e3, kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=cmap)\n", - "axs[0, 1].set_title('x-z plane')\n", - "axs[0, 1].axis('image')\n", - "\n", - "axs[1, 0].imshow(p0_recon[N[0] // 2, :, :],\n", - " extent=[kgrid.z_vec.min() * 1e3, kgrid.z_vec.max() * 1e3, kgrid.y_vec.max() * 1e3, kgrid.y_vec.min() * 1e3],\n", - " vmin=plot_scale[0], vmax=plot_scale[1], cmap=cmap)\n", - "axs[1, 0].set_title('y-z plane')\n", - "axs[1, 0].axis('image')\n", - "\n", - "axs[1, 1].axis('off')\n", - "axs[1, 1].set_title('(All axes in mm)')\n", - "\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/sd_directivity_modelling_2D/sd_directivity_modelling_2D.py b/examples/sd_directivity_modelling_2D.py similarity index 81% rename from examples/sd_directivity_modelling_2D/sd_directivity_modelling_2D.py rename to examples/sd_directivity_modelling_2D.py index d7b21e8f7..bc2fa5f32 100644 --- a/examples/sd_directivity_modelling_2D/sd_directivity_modelling_2D.py +++ b/examples/sd_directivity_modelling_2D.py @@ -1,7 +1,8 @@ -import os -from copy import deepcopy -from tempfile import gettempdir +# %% [markdown] +# # Sensor Directivity Modelling in 2D +# Model the directional response of a finite-sized sensor element in 2D. +# %% import matplotlib.pyplot as plt import numpy as np @@ -10,9 +11,7 @@ from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor from kwave.ksource import kSource -from kwave.kspaceFirstOrder2D import kspaceFirstOrder2DC -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.utils.colormap import get_color_map from kwave.utils.conversion import cart2grid from kwave.utils.data import scale_SI @@ -20,6 +19,7 @@ from kwave.utils.mapgen import make_cart_circle from kwave.utils.matlab import ind2sub, matlab_find, unflatten_matlab_mask +# %% grid_size_points = Vector([128, 128]) # [grid points] grid_size_meters = Vector([50e-3, 50e-3]) # [m] grid_spacing_meters = grid_size_meters / grid_size_points # [m] @@ -60,6 +60,7 @@ # filter the source to remove high frequencies not supported by the grid source.p = filter_time_series(kgrid, medium, source.p) +# %% # pre-allocate array for storing the output time series single_element_data = np.zeros((Nt, points)) # noqa: F841 @@ -71,22 +72,17 @@ source.p_mask[unflatten_matlab_mask(source.p_mask, source_positions[source_loop] - 1)] = 1 # 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) - # run the simulation - sensor_data = kspaceFirstOrder2DC( - medium=medium, - kgrid=kgrid, - source=deepcopy(source), - sensor=deepcopy(sensor), - simulation_options=simulation_options, - execution_options=SimulationExecutionOptions(), + sensor_data = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + backend="cpp", + device="cpu", ) - single_element_data[:, source_loop] = sensor_data["p"].sum(axis=1) + single_element_data[:, source_loop] = sensor_data["p"].sum(axis=0) +# %% plt.figure() plt.imshow( circle + sensor.mask, diff --git a/examples/sd_directivity_modelling_2D/README.md b/examples/sd_directivity_modelling_2D/README.md deleted file mode 100644 index 67cebfd21..000000000 --- a/examples/sd_directivity_modelling_2D/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Modelling Sensor Directivity In 2D Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/sd_directivity_modelling_2D/sd_directivity_modelling_2D.ipynb) - -This example demonstrates how the sensitivity of a large single element detector varies with the angular position of a point-like source. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_sd_directivity_modelling_2D.php). diff --git a/examples/sd_directivity_modelling_2D/sd_directivity_modelling_2D.ipynb b/examples/sd_directivity_modelling_2D/sd_directivity_modelling_2D.ipynb deleted file mode 100644 index 63ccc5167..000000000 --- a/examples/sd_directivity_modelling_2D/sd_directivity_modelling_2D.ipynb +++ /dev/null @@ -1,244 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install git+https://github.com/waltsims/k-wave-python " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Modelling Sensor Directivity In 2D Example\n", - "\n", - "This example demonstrates how the sensitivity of a large single element detector varies with the angular position of a point-like source." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "\n", - "import os\n", - "from copy import deepcopy\n", - "from tempfile import gettempdir\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder2D import kspaceFirstOrder2DC\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.colormap import get_color_map\n", - "from kwave.utils.conversion import cart2grid\n", - "from kwave.utils.data import scale_SI\n", - "from kwave.utils.filters import filter_time_series\n", - "from kwave.utils.mapgen import make_cart_circle\n", - "from kwave.utils.matlab import ind2sub, matlab_find, unflatten_matlab_mask" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create the computational grid and medium" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "grid_size_points = Vector([128, 128]) # [grid points]\n", - "grid_size_meters = Vector([50e-3, 50e-3]) # [m]\n", - "grid_spacing_meters = grid_size_meters / grid_size_points # [m]\n", - "kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)\n", - "\n", - "# define the properties of the propagation medium\n", - "medium = kWaveMedium(sound_speed=1500)\n", - "\n", - "# define the array of time points [s]\n", - "Nt = 350\n", - "dt = 7e-8 # [s]\n", - "kgrid.setTime(Nt, dt)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define a large area detector" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sz = 20 # [grid points]\n", - "sensor_mask = np.zeros(grid_size_points)\n", - "sensor_mask[grid_size_points.x // 2, (grid_size_points.y // 2 - sz // 2) : (grid_size_points.y // 2 + sz // 2) + 1] = 1\n", - "sensor = kSensor(sensor_mask)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define a time varying sinusoidal source" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define equally spaced point sources lying on a circle centred at the\n", - "# centre of the detector face\n", - "radius = 30 # [grid points]\n", - "points = 11\n", - "circle = make_cart_circle(radius * grid_spacing_meters.x, points, Vector([0, 0]), np.pi)\n", - "\n", - "# find the binary sensor mask most closely corresponding to the Cartesian\n", - "# coordinates from makeCartCircle\n", - "circle, _, _ = cart2grid(kgrid, circle)\n", - "\n", - "# find the indices of the sources in the binary source mask\n", - "source_positions = matlab_find(circle, val=1, mode=\"eq\")\n", - "\n", - "source = kSource()\n", - "source_freq = 0.25e6 # [Hz]\n", - "source_mag = 1 # [Pa]\n", - "source.p = source_mag * np.sin(2 * np.pi * source_freq * kgrid.t_array)\n", - "\n", - "# filter the source to remove high frequencies not supported by the grid\n", - "source.p = filter_time_series(kgrid, medium, source.p)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define simulation parameters and run simulations" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# pre-allocate array for storing the output time series\n", - "single_element_data = np.zeros((Nt, points)) # noqa: F841\n", - "\n", - "# run a simulation for each of these sources to see the effect that the\n", - "# angle from the detector has on the measured signal\n", - "for source_loop in range(points):\n", - " # select a point source\n", - " source.p_mask = np.zeros(grid_size_points)\n", - " source.p_mask[unflatten_matlab_mask(source.p_mask, source_positions[source_loop] - 1)] = 1\n", - "\n", - " # run the simulation\n", - "\n", - " input_filename = f\"example_input_{source_loop + 1}_input.h5\"\n", - " pathname = gettempdir()\n", - " input_file_full_path = os.path.join(pathname, input_filename)\n", - " simulation_options = SimulationOptions(save_to_disk=True, input_filename=input_filename, data_path=pathname)\n", - " # run the simulation\n", - " sensor_data = kspaceFirstOrder2DC(\n", - " medium=medium,\n", - " kgrid=kgrid,\n", - " source=deepcopy(source),\n", - " sensor=deepcopy(sensor),\n", - " simulation_options=simulation_options,\n", - " execution_options=SimulationExecutionOptions(),\n", - " )\n", - " single_element_data[:, source_loop] = sensor_data['p'].sum(axis=1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualize recorded data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure()\n", - "plt.imshow(circle + sensor.mask, \n", - " extent=[\n", - " kgrid.y_vec.min() * 1e3, kgrid.y_vec.max() * 1e3, \n", - " kgrid.x_vec.max() * 1e3, kgrid.x_vec.min() * 1e3], vmin=-1, vmax=1, cmap=get_color_map())\n", - "plt.ylabel('x-position [mm]')\n", - "plt.xlabel('y-position [mm]')\n", - "plt.axis('image')\n", - "\n", - "_, t_scale, t_prefix, _ = scale_SI(kgrid.t_array[-1])\n", - "\n", - "# Plot the time series recorded for each of the sources\n", - "plt.figure()\n", - "# Loop through each source and plot individually\n", - "for i in range(single_element_data.shape[1]):\n", - " plt.plot((kgrid.t_array * t_scale).squeeze(), single_element_data[:, i], label=f'Source {i+1}')\n", - "\n", - "plt.xlabel(f'Time [{t_prefix}s]')\n", - "plt.ylabel('Pressure [au]')\n", - "plt.title('Time Series For Each Source Direction')\n", - "\n", - "\n", - "# Calculate angle between source and center of detector face\n", - "angles = []\n", - "for source_position in source_positions:\n", - " x, y = ind2sub(kgrid.y.shape, source_position)\n", - " angles.append(np.arctan(kgrid.y[x, y] / kgrid.x[x, y]))\n", - "\n", - "# Plot the maximum amplitudes for each of the sources\n", - "plt.figure()\n", - "plt.plot(angles, np.max(single_element_data[200:350, :], axis=0), 'o', mfc='none')\n", - "plt.xlabel('Angle Between Source and Centre of Detector Face [rad]')\n", - "plt.ylabel('Maximum Detected Pressure [au]')\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env_kwave", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/sd_focussed_detector_2D/sd_focussed_detector_2D.py b/examples/sd_focussed_detector_2D.py similarity index 53% rename from examples/sd_focussed_detector_2D/sd_focussed_detector_2D.py rename to examples/sd_focussed_detector_2D.py index 38091b941..1f9a706ce 100644 --- a/examples/sd_focussed_detector_2D/sd_focussed_detector_2D.py +++ b/examples/sd_focussed_detector_2D.py @@ -1,10 +1,8 @@ +# %% [markdown] # # 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 +# Model the output of a focused semicircular detector with spatial averaging across its surface. +# %% import matplotlib.pyplot as plt import numpy as np @@ -12,16 +10,12 @@ from kwave.kgrid import kWaveGrid from kwave.kmedium import kWaveMedium from kwave.ksource import kSource -from kwave.kspaceFirstOrder2D import kspaceFirstOrder2DC +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.ktransducer import kSensor -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions from kwave.utils.data import scale_SI from kwave.utils.mapgen import make_circle, make_disc -# In[3]: - - +# %% # create the computational grid grid_size = Vector([180, 180]) # [grid points] grid_spacing = Vector([0.1e-3, 0.1e-3]) # [m] @@ -40,54 +34,45 @@ t_end = 11e-6 # [s] _ = kgrid.makeTime(medium.sound_speed, t_end=t_end) - -# ## 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) - - -# ## Run simulation with first source +# %% +## Run simulation with first source # place a disc-shaped source near the focus of the detector source = kSource() source.p0 = 2 * make_disc(grid_size, grid_size / 2, 4) # run the simulation -sensor_data1 = kspaceFirstOrder2DC( - medium=medium, - kgrid=kgrid, - source=deepcopy(source), - sensor=sensor, - simulation_options=simulation_options, - execution_options=SimulationExecutionOptions(), +sensor_data1 = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + backend="cpp", + device="cpu", ) sensor_data1["p"].shape - -# ## Run simulation with second source - +# %% +## Run simulation with second source # place a disc-shaped source horizontally shifted from the focus of the detector source.p0 = 2 * make_disc(grid_size, grid_size / 2 + [0, 20], 4) -sensor_data2 = kspaceFirstOrder2DC( - medium=medium, - kgrid=kgrid, - source=deepcopy(source), - sensor=sensor, - simulation_options=simulation_options, - execution_options=SimulationExecutionOptions(), +sensor_data2 = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + backend="cpp", + device="cpu", ) +# %% +## Visualize recorded data -# ## Visualize recorded data - - -sensor_output1 = np.sum(sensor_data1["p"], axis=1) / np.sum(sensor.mask) -sensor_output2 = np.sum(sensor_data2["p"], axis=1) / np.sum(sensor.mask) +sensor_output1 = np.sum(sensor_data1["p"], axis=0) / np.sum(sensor.mask) +sensor_output2 = np.sum(sensor_data2["p"], axis=0) / np.sum(sensor.mask) t_sc, t_scale, t_prefix, _ = scale_SI(t_end) t_array = kgrid.t_array.squeeze() * t_scale diff --git a/examples/sd_focussed_detector_2D/README.md b/examples/sd_focussed_detector_2D/README.md deleted file mode 100644 index 3d78d3b76..000000000 --- a/examples/sd_focussed_detector_2D/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Focussed Detector In 2D Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/sd_focussed_detector_2D/sd_focussed_detector_2D.ipynb) - -This example shows how k-Wave-python can be used to model the output of a focussed semicircular detector where the directionality arises from spatially averaging across the detector surface. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_sd_focussed_detector_2D.php). diff --git a/examples/sd_focussed_detector_2D/sd_focussed_detector_2D.ipynb b/examples/sd_focussed_detector_2D/sd_focussed_detector_2D.ipynb deleted file mode 100644 index 6c54e5cfe..000000000 --- a/examples/sd_focussed_detector_2D/sd_focussed_detector_2D.ipynb +++ /dev/null @@ -1,207 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install git+https://github.com/waltsims/k-wave-python " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Focussed Detector In 2D Example\n", - "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." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "\n", - "import os\n", - "from copy import deepcopy\n", - "from tempfile import gettempdir\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder2D import kspaceFirstOrder2DC\n", - "from kwave.ktransducer import kSensor\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.data import scale_SI\n", - "from kwave.utils.mapgen import make_circle, make_disc\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create the computational grid\n", - "grid_size = Vector([180, 180]) # [grid points]\n", - "grid_spacing = Vector([0.1e-3, 0.1e-3]) # [m]\n", - "kgrid = kWaveGrid(grid_size, grid_spacing)\n", - "\n", - "# define the properties of the propagation medium\n", - "medium = kWaveMedium(sound_speed=1500)\n", - "\n", - "# define a sensor as part of a circle centred on the grid\n", - "sensor_radius = 65 # [grid points]\n", - "arc_angle = np.pi # [rad]\n", - "sensor_mask = make_circle(grid_size, grid_size // 2 + 1, sensor_radius, arc_angle)\n", - "sensor = kSensor(sensor_mask)\n", - "\n", - "# define the array of temporal points\n", - "t_end = 11e-6 # [s]\n", - "_ = kgrid.makeTime(medium.sound_speed, t_end=t_end)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define simulation parameters" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "input_filename = \"example_sd_focused_2d_input.h5\"\n", - "pathname = gettempdir()\n", - "input_file_full_path = os.path.join(pathname, input_filename)\n", - "simulation_options = SimulationOptions(save_to_disk=True, input_filename=input_filename, data_path=pathname)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run simulation with first source" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# place a disc-shaped source near the focus of the detector\n", - "source = kSource()\n", - "source.p0 = 2 * make_disc(grid_size, grid_size / 2, 4)\n", - "\n", - "# run the simulation\n", - "sensor_data1 = kspaceFirstOrder2DC(\n", - " medium=medium,\n", - " kgrid=kgrid,\n", - " source=deepcopy(source),\n", - " sensor=sensor,\n", - " simulation_options=simulation_options,\n", - " execution_options=SimulationExecutionOptions(),\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sensor_data1['p'].shape\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run simulation with second source" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# place a disc-shaped source horizontally shifted from the focus of the detector\n", - "source.p0 = 2 * make_disc(grid_size, grid_size / 2 + [0, 20], 4)\n", - "\n", - "sensor_data2 = kspaceFirstOrder2DC(\n", - " medium=medium,\n", - " kgrid=kgrid,\n", - " source=deepcopy(source),\n", - " sensor=sensor,\n", - " simulation_options=simulation_options,\n", - " execution_options=SimulationExecutionOptions(),\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualize recorded data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sensor_output1 = np.sum(sensor_data1['p'], axis=1) / np.sum(sensor.mask)\n", - "sensor_output2 = np.sum(sensor_data2['p'], axis=1) / np.sum(sensor.mask)\n", - "\n", - "t_sc, t_scale, t_prefix, _ = scale_SI(t_end)\n", - "t_array = kgrid.t_array.squeeze() * t_scale\n", - "\n", - "plt.plot(t_array, sensor_output1, 'k')\n", - "plt.plot(t_array, sensor_output2, 'r')\n", - "\n", - "plt.xlabel('Time [' + t_prefix + 's]')\n", - "plt.ylabel('Average Pressure Measured Over Detector [au]')\n", - "plt.legend([\n", - " f\"Source on focus, sum(output^2) = {round(np.sum(sensor_output1**2) * 100) / 100}\",\n", - " f\"Source off focus, sum(output^2) = {round(np.sum(sensor_output2**2) * 100) / 100}\"\n", - "])\n", - "\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env_kwave", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/sd_focussed_detector_3D/sd_focussed_detector_3D.py b/examples/sd_focussed_detector_3D.py similarity index 69% rename from examples/sd_focussed_detector_3D/sd_focussed_detector_3D.py rename to examples/sd_focussed_detector_3D.py index 03e5fd6e5..adde71f30 100644 --- a/examples/sd_focussed_detector_3D/sd_focussed_detector_3D.py +++ b/examples/sd_focussed_detector_3D.py @@ -1,10 +1,8 @@ -# 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 +# %% [markdown] +# # Focussed Detector In 3D Example +# Model the output of a focussed bowl detector with spatial averaging across its surface. +# %% import matplotlib.pyplot as plt import numpy as np @@ -12,14 +10,13 @@ from kwave.kgrid import kWaveGrid from kwave.kmedium import kWaveMedium from kwave.ksource import kSource -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.ktransducer import kSensor -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions from kwave.utils.data import scale_SI from kwave.utils.filters import filter_time_series from kwave.utils.mapgen import make_bowl +# %% # create the computational grid grid_size = Vector([64, 64, 64]) # [grid points] grid_spacing_single = 100e-3 / grid_size.x @@ -32,13 +29,6 @@ # define the array of temporal points _ = 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" -) - # create a concave sensor sphere_offset = 10 diameter = grid_size.x / 2 + 1 @@ -57,6 +47,7 @@ source.p = source_mag * np.sin(2 * np.pi * source_freq * kgrid.t_array) source.p = filter_time_series(kgrid, medium, source.p) +# %% # place the first point source near the focus of the detector source1 = np.zeros(grid_size) source1[int(sphere_offset + radius), grid_size.y // 2 - 1, grid_size.z // 2 - 1] = 1 @@ -64,18 +55,20 @@ # run the first simulation source.p_mask = source1 -sensor_data1 = kspaceFirstOrder3D( - medium=medium, - kgrid=kgrid, - source=deepcopy(source), - sensor=deepcopy(sensor), - simulation_options=simulation_options, - execution_options=SimulationExecutionOptions(is_gpu_simulation=False), +sensor_data1 = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + backend="cpp", + device="cpu", + pml_size=10, ) # average the data recorded at each grid point to simulate the measured signal from a single element focused detector -sensor_data1 = np.sum(sensor_data1["p"], axis=1) +sensor_data1 = np.sum(sensor_data1["p"], axis=0) +# %% # place the second point source off axis source2 = np.zeros(grid_size) source2[int(1 + sphere_offset + radius), grid_size.y // 2 + 5, grid_size.z // 2 + 5] = 1 @@ -83,18 +76,20 @@ # run the second simulation source.p_mask = source2 -sensor_data2 = kspaceFirstOrder3D( - medium=medium, - kgrid=kgrid, - source=source, - sensor=sensor, - simulation_options=simulation_options, - execution_options=SimulationExecutionOptions(is_gpu_simulation=False), +sensor_data2 = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + backend="cpp", + device="cpu", + pml_size=10, ) # average the data recorded at each grid point to simulate the measured signal from a single element focused detector -sensor_data2 = np.sum(sensor_data2["p"], axis=1) +sensor_data2 = np.sum(sensor_data2["p"], axis=0) +# %% # Combine arrays as in MATLAB: sensor.mask + source1 + source2 combined_array = sensor_mask + source1 + source2 diff --git a/examples/sd_focussed_detector_3D/README.md b/examples/sd_focussed_detector_3D/README.md deleted file mode 100644 index de30d13e3..000000000 --- a/examples/sd_focussed_detector_3D/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Focussed Detector In 3D Example - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/sd_focussed_detector_3D/sd_focussed_detector_3D.ipynb) - -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. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_sd_focussed_detector_3D.php). diff --git a/examples/sd_focussed_detector_3D/sd_focussed_detector_3D.ipynb b/examples/sd_focussed_detector_3D/sd_focussed_detector_3D.ipynb deleted file mode 100644 index bf427846e..000000000 --- a/examples/sd_focussed_detector_3D/sd_focussed_detector_3D.ipynb +++ /dev/null @@ -1,275 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install git+https://github.com/waltsims/k-wave-python " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Focussed Detector In 3D Example\n", - "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." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "\n", - "import os\n", - "from copy import deepcopy\n", - "from tempfile import gettempdir\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksource import kSource\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.ktransducer import kSensor\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.utils.data import scale_SI\n", - "from kwave.utils.filters import filter_time_series\n", - "from kwave.utils.mapgen import make_bowl" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create the computational grid\n", - "grid_size = Vector([64, 64, 64]) # [grid points]\n", - "grid_spacing_single = 100e-3 / grid_size.x\n", - "grid_spacing = Vector([grid_spacing_single, grid_spacing_single, grid_spacing_single]) # [m]\n", - "kgrid = kWaveGrid(grid_size, grid_spacing)\n", - "\n", - "# define the properties of the propagation medium\n", - "medium = kWaveMedium(sound_speed=1500)\n", - "\n", - "# define the array of temporal points\n", - "_ = kgrid.makeTime(medium.sound_speed)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define simulation parameters" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "input_filename = \"example_sd_focused_3d_input.h5\"\n", - "pathname = gettempdir()\n", - "input_file_full_path = os.path.join(pathname, input_filename)\n", - "simulation_options = SimulationOptions(\n", - " save_to_disk=True, \n", - " input_filename=input_filename, \n", - " data_path=pathname, \n", - " pml_size=10, \n", - " data_cast='single'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define a concave sensor" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# create a concave sensor\n", - "sphere_offset = 10\n", - "diameter = grid_size.x / 2 + 1\n", - "radius = grid_size.x / 2\n", - "bowl_pos = Vector([1 + sphere_offset, grid_size.y / 2, grid_size.z / 2])\n", - "focus_pos = grid_size / 2\n", - "\n", - "sensor_mask = make_bowl(grid_size, bowl_pos, radius, diameter, focus_pos)\n", - "sensor = kSensor(sensor_mask)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run simulation with first source" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define a time varying sinusoidal source\n", - "source = kSource()\n", - "\n", - "source_freq = 0.25e6 # [Hz]\n", - "source_mag = 1 # [Pa]\n", - "source.p = source_mag * np.sin(2 * np.pi * source_freq * kgrid.t_array)\n", - "source.p = filter_time_series(kgrid, medium, source.p)\n", - "\n", - "# place the first point source near the focus of the detector\n", - "source1 = np.zeros(grid_size)\n", - "source1[int(sphere_offset + radius), grid_size.y // 2 - 1, grid_size.z // 2 - 1] = 1\n", - "\n", - "# run the first simulation\n", - "source.p_mask = source1\n", - "\n", - "sensor_data1 = kspaceFirstOrder3D(\n", - " medium=medium,\n", - " kgrid=kgrid,\n", - " source=deepcopy(source),\n", - " sensor=deepcopy(sensor),\n", - " simulation_options=simulation_options,\n", - " execution_options=SimulationExecutionOptions(is_gpu_simulation=False),\n", - ")\n", - "\n", - "# average the data recorded at each grid point to simulate the measured signal from a single element focused detector\n", - "sensor_data1 = np.sum(sensor_data1['p'], axis=1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run simulation with second source" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# place the second point source off axis\n", - "source2 = np.zeros(grid_size)\n", - "source2[int(1 + sphere_offset + radius), grid_size.y // 2 + 5, grid_size.z // 2 + 5] = 1\n", - "\n", - "\n", - "# run the second simulation\n", - "source.p_mask = source2\n", - "sensor_data2 = kspaceFirstOrder3D(\n", - " medium=medium,\n", - " kgrid=kgrid,\n", - " source=source,\n", - " sensor=sensor,\n", - " simulation_options=simulation_options,\n", - " execution_options=SimulationExecutionOptions(is_gpu_simulation=False),\n", - ")\n", - "\n", - "# average the data recorded at each grid point to simulate the measured signal from a single element focused detector\n", - "sensor_data2 = np.sum(sensor_data2['p'], axis=1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualize detector and sources" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Combine arrays as in MATLAB: sensor.mask + source1 + source2\n", - "combined_array = sensor_mask + source1 + source2\n", - "\n", - "# Find the indices of non-zero elements\n", - "x, y, z = np.nonzero(combined_array)\n", - "\n", - "# Enable interactive mode\n", - "plt.ion()\n", - "\n", - "# Create an interactive 3D plot with matplotlib\n", - "fig = plt.figure(figsize=(8, 6))\n", - "ax = fig.add_subplot(111, projection='3d')\n", - "sc = ax.scatter(x, y, z, c='blue', marker='s', s=100, depthshade=True) # 's' for square-like markers\n", - "\n", - "# Set the view angle to mimic MATLAB's `view([130, 40])`\n", - "ax.view_init(elev=40, azim=130)\n", - "\n", - "# Customize the axes\n", - "ax.set_xlabel('X')\n", - "ax.set_ylabel('Y')\n", - "ax.set_zlabel('Z')\n", - "ax.set_title('Interactive 3D Voxel Plot')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualize recorded data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "t_sc, t_scale, t_prefix, _ = scale_SI(kgrid.t_array[-1])\n", - "t_array = kgrid.t_array.squeeze() * t_scale\n", - "\n", - "plt.figure()\n", - "plt.plot(t_array, sensor_data1)\n", - "plt.plot(t_array, sensor_data2, 'r')\n", - "\n", - "plt.xlabel('Time [' + t_prefix + 's]')\n", - "plt.ylabel('Average Pressure Measured By Focussed Detector [Pa]')\n", - "plt.legend(['Source on axis', 'Source off axis'])\n", - "\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env_kwave", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/unified_entry_point_demo.py b/examples/unified_entry_point_demo.py index fbf35e6bf..2f3710719 100644 --- a/examples/unified_entry_point_demo.py +++ b/examples/unified_entry_point_demo.py @@ -1,3 +1,8 @@ +# %% [markdown] +# # Unified Entry Point Demo +# Demonstrates the new single-function kspaceFirstOrder() API across backends and PML options. + +# %% """ Unified Entry Point Demo — kspaceFirstOrder() @@ -13,9 +18,7 @@ from kwave.ksource import kSource from kwave.kspaceFirstOrder import kspaceFirstOrder -# ============================================================ -# Setup (same for all backends) -# ============================================================ +# %% Common setup grid_size = Vector([128, 128]) kgrid = kWaveGrid(grid_size, Vector([0.1e-3, 0.1e-3])) kgrid.makeTime(1500) @@ -29,47 +32,41 @@ sensor = kSensor(mask=np.ones((128, 128), dtype=bool)) -# ============================================================ -# Example 1: Native CPU (default) -# ============================================================ +# %% Example 1: Native CPU (default) print("Running native CPU simulation...") result = kspaceFirstOrder(kgrid, medium, source, sensor) print(f" Sensor data: {result['p'].shape}") -# ============================================================ -# Example 2: Custom PML -# ============================================================ +# %% Example 2: Custom PML print("\nRunning with custom PML (10, 15)...") result = kspaceFirstOrder(kgrid, medium, source, sensor, pml_size=(10, 15)) print(f" Sensor data: {result['p'].shape}") -# ============================================================ -# Example 3: Auto PML -# ============================================================ +# %% Example 3: Auto PML print("\nRunning with auto PML...") result = kspaceFirstOrder(kgrid, medium, source, sensor, pml_size="auto") print(f" Sensor data: {result['p'].shape}") -# ============================================================ -# Example 4: C++ save_only (writes HDF5 for cluster submission) -# ============================================================ +# %% Example 4: C++ save_only (writes HDF5 for cluster submission) +import os import tempfile -print("\nSaving HDF5 for C++ binary...") -result = kspaceFirstOrder( - kgrid, - medium, - source, - sensor, - backend="cpp", - save_only=True, - data_path=tempfile.mkdtemp(), -) -print(f" Input file: {result['input_file']}") +if os.environ.get("KWAVE_BACKEND", "").lower() == "python": + print("\nSkipping C++ save_only example (KWAVE_BACKEND=python)...") +else: + print("\nSaving HDF5 for C++ binary...") + result = kspaceFirstOrder( + kgrid, + medium, + source, + sensor, + backend="cpp", + save_only=True, + data_path=tempfile.mkdtemp(), + ) + print(f" Input file: {result['input_file']}") -# ============================================================ -# Example 5: Migrating from legacy options -# ============================================================ +# %% Example 5: Migrating from legacy options print("\nMigrating from legacy options...") from kwave.compat import options_to_kwargs from kwave.options.simulation_options import SimulationOptions diff --git a/examples/us_beam_patterns/us_beam_patterns.py b/examples/us_beam_patterns.py similarity index 88% rename from examples/us_beam_patterns/us_beam_patterns.py rename to examples/us_beam_patterns.py index 4afa0269e..1efc9c1db 100644 --- a/examples/us_beam_patterns/us_beam_patterns.py +++ b/examples/us_beam_patterns.py @@ -1,30 +1,23 @@ -#!/usr/bin/env python -# coding: utf-8 - -# In[ ]: - +# %% [markdown] +# # Ultrasound Beam Patterns Example +# Compute and visualize beam patterns from a linear transducer array in 3D. +# %% import matplotlib.pyplot as plt import numpy as np -from kwave.data import Vector from kwave.kgrid import kWaveGrid from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.ktransducer import NotATransducer, kWaveTransducerSimple -from kwave.kWaveSimulation import SimulationOptions -from kwave.options.simulation_execution_options import SimulationExecutionOptions from kwave.utils.dotdictionary import dotdict from kwave.utils.filters import spect from kwave.utils.math import find_closest from kwave.utils.signals import tone_burst -# In[ ]: - - +# %% # simulation settings -DATA_CAST = "single" # set to 'xy' or 'xz' to generate the beam pattern in different planes MASK_PLANE = "xy" # set to true to compute the rms or peak beam patterns, set to false to compute the harmonic beam patterns @@ -44,10 +37,7 @@ kgrid = kWaveGrid([Nx, Ny, Nz], [dx, dy, dz]) - -# In[ ]: - - +# %% # define the medium medium = kWaveMedium(sound_speed=1540, density=1000, alpha_coeff=0.75, alpha_power=1.5, BonA=6) @@ -56,10 +46,6 @@ kgrid.makeTime(medium.sound_speed, t_end=t_end) - -# In[ ]: - - # define the input signal source_strength = 1e6 tone_burst_freq = 0.5e6 @@ -67,10 +53,7 @@ input_signal = tone_burst(1 / kgrid.dt, tone_burst_freq, tone_burst_cycles) input_signal = (source_strength / (medium.sound_speed * medium.density)) * input_signal - -# In[ ]: - - +# %% # define the transducer transducer = dotdict() transducer.number_elements = 32 @@ -86,10 +69,6 @@ transducer.position = np.round([1, Ny / 2 - transducer_width / 2, Nz / 2 - transducer.element_length / 2]) transducer = kWaveTransducerSimple(kgrid, **transducer) - -# In[ ]: - - not_transducer = dotdict() not_transducer.sound_speed = medium.sound_speed # sound speed [m/s] not_transducer.focus_distance = 20e-3 # focus distance [m] @@ -102,10 +81,7 @@ not_transducer = NotATransducer(transducer, kgrid, **not_transducer) - -# In[ ]: - - +# %% sensor_mask = np.zeros((Nx, Ny, Nz)) if MASK_PLANE == "xy": @@ -121,39 +97,23 @@ j_vec = kgrid.z_vec j_label = "z" - -# In[ ]: - - sensor = kSensor(sensor_mask) if USE_STATISTICS: sensor.record = ["p_rms", "p_max"] -simulation_options = SimulationOptions( - pml_inside=False, - pml_size=Vector([PML_X_SIZE, PML_Y_SIZE, PML_Z_SIZE]), - data_cast=DATA_CAST, - save_to_disk=True, -) - -if not USE_STATISTICS: - simulation_options.stream_to_disk = "harmonic_data.h5" - -execution_options = SimulationExecutionOptions(is_gpu_simulation=True) - -sensor_data = kspaceFirstOrder3D( - medium=medium, - kgrid=kgrid, - source=not_transducer, - sensor=sensor, - simulation_options=simulation_options, - execution_options=execution_options, +# NOTE: pml_inside=False not supported in new API +# NOTE: stream_to_disk not supported in new API +sensor_data = kspaceFirstOrder( + kgrid, + medium, + not_transducer, + sensor, + backend="cpp", + device="gpu", + pml_size=(PML_X_SIZE, PML_Y_SIZE, PML_Z_SIZE), ) - -# In[ ]: - - +# %% if USE_STATISTICS: fig, axes = plt.subplots(1, 2) fig.set_figwidth(8) @@ -183,10 +143,7 @@ plt.tight_layout() plt.show() - -# In[ ]: - - +# %% if not USE_STATISTICS: sensor_data_array = np.reshape(sensor_data["p"], [kgrid.Nt, kgrid.Ny, kgrid.Nx]).transpose(2, 1, 0) # compute the amplitude spectrum @@ -205,10 +162,7 @@ # extract the integral of the total amplitude spectrum beam_pattern_total = np.squeeze(np.sum(amp_spect, axis=2)) - -# In[ ]: - - +# %% if not USE_STATISTICS: fig, axes = plt.subplots(1, 3) fig.set_figwidth(8) @@ -239,10 +193,7 @@ plt.tight_layout() plt.show() - -# In[ ]: - - +# %% if not USE_STATISTICS: # Compute the directivity at each of the harmonics directivity_f1 = beam_pattern_f1[round(not_transducer.focus_distance / dx), :] diff --git a/examples/us_beam_patterns/README.md b/examples/us_beam_patterns/README.md deleted file mode 100644 index b66238c3f..000000000 --- a/examples/us_beam_patterns/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Simulating Ultrasound Beam Patterns Example - - [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/us_beam_patterns/us_beam_patterns.ipynb) - -This example shows how the nonlinear beam pattern from an ultrasound transducer can be modelled. It builds on the Defining An Ultrasound Transducer and Simulating Transducer Field Patterns examples. - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_us_beam_patterns.php). \ No newline at end of file diff --git a/examples/us_beam_patterns/us_beam_patterns.ipynb b/examples/us_beam_patterns/us_beam_patterns.ipynb deleted file mode 100644 index c267971cb..000000000 --- a/examples/us_beam_patterns/us_beam_patterns.ipynb +++ /dev/null @@ -1,345 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "# !pip install k-wave-python" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.ktransducer import NotATransducer, kWaveTransducerSimple\n", - "from kwave.kWaveSimulation import SimulationOptions\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.utils.dotdictionary import dotdict\n", - "from kwave.utils.filters import spect\n", - "from kwave.utils.math import find_closest\n", - "from kwave.utils.signals import tone_burst" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# simulation settings\n", - "DATA_CAST = \"single\"\n", - "# set to 'xy' or 'xz' to generate the beam pattern in different planes\n", - "MASK_PLANE = \"xy\"\n", - "# set to true to compute the rms or peak beam patterns, set to false to compute the harmonic beam patterns\n", - "USE_STATISTICS = True\n", - "\n", - "# define the grid\n", - "PML_X_SIZE = 20\n", - "PML_Y_SIZE = 10\n", - "PML_Z_SIZE = 10\n", - "Nx = 128 - 2 * PML_X_SIZE\n", - "Ny = 64 - 2 * PML_Y_SIZE\n", - "Nz = 64 - 2 * PML_Z_SIZE\n", - "x = 40e-3\n", - "dx = x / Nx\n", - "dy = dx\n", - "dz = dx\n", - "\n", - "kgrid = kWaveGrid([Nx, Ny, Nz], [dx, dy, dz])\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define the medium\n", - "medium = kWaveMedium(sound_speed=1540, density=1000, alpha_coeff=0.75, alpha_power=1.5, BonA=6)\n", - "\n", - "# create the time array - using a time == time to travel the hypot of the grid\n", - "t_end = 45e-6 # alternatively, use np.sqrt(kgrid.x_size ** 2 + kgrid.y_size ** 2) / medium.sound_speed\n", - "\n", - "kgrid.makeTime(medium.sound_speed, t_end=t_end)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define the input signal\n", - "source_strength = 1e6\n", - "tone_burst_freq = 0.5e6\n", - "tone_burst_cycles = 5\n", - "input_signal = tone_burst(1 / kgrid.dt, tone_burst_freq, tone_burst_cycles)\n", - "input_signal = (source_strength / (medium.sound_speed * medium.density)) * input_signal\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define the transducer\n", - "transducer = dotdict()\n", - "transducer.number_elements = 32\n", - "transducer.element_width = 1\n", - "transducer.element_length = 12\n", - "transducer.element_spacing = 0\n", - "transducer.radius = np.inf\n", - "\n", - "# calculate the width of the transducer in grid points\n", - "transducer_width = transducer.number_elements * transducer.element_width + (transducer.number_elements - 1) * transducer.element_spacing\n", - "\n", - "# use this to position the transducer in the middle of the computational grid\n", - "transducer.position = np.round([1, Ny / 2 - transducer_width / 2, Nz / 2 - transducer.element_length / 2])\n", - "transducer = kWaveTransducerSimple(kgrid, **transducer)\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "not_transducer = dotdict()\n", - "not_transducer.sound_speed = medium.sound_speed # sound speed [m/s]\n", - "not_transducer.focus_distance = 20e-3 # focus distance [m]\n", - "not_transducer.elevation_focus_distance = 19e-3 # focus distance in the elevation plane [m]\n", - "not_transducer.steering_angle = 0 # steering angle [degrees]\n", - "not_transducer.transmit_apodization = \"Rectangular\"\n", - "not_transducer.receive_apodization = \"Rectangular\"\n", - "not_transducer.active_elements = np.ones((transducer.number_elements, 1))\n", - "not_transducer.input_signal = input_signal\n", - "\n", - "not_transducer = NotATransducer(transducer, kgrid, **not_transducer)\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sensor_mask = np.zeros((Nx, Ny, Nz))\n", - "\n", - "if MASK_PLANE == \"xy\":\n", - " sensor_mask[:, :, Nz // 2] = 1\n", - " # store y axis properties\n", - " Nj = Ny\n", - " j_vec = kgrid.y_vec\n", - " j_label = \"y\"\n", - "elif MASK_PLANE == \"xz\":\n", - " sensor_mask[:, Ny // 2, :] = 1\n", - " # store z axis properties\n", - " Nj = Nz\n", - " j_vec = kgrid.z_vec\n", - " j_label = \"z\"\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sensor = kSensor(sensor_mask)\n", - "if USE_STATISTICS:\n", - " sensor.record = [\"p_rms\", \"p_max\"]\n", - "\n", - "simulation_options = SimulationOptions(\n", - " pml_inside=False,\n", - " pml_size=Vector([PML_X_SIZE, PML_Y_SIZE, PML_Z_SIZE]),\n", - " data_cast=DATA_CAST,\n", - " save_to_disk=True,\n", - ")\n", - "\n", - "if not USE_STATISTICS:\n", - " simulation_options.stream_to_disk = \"harmonic_data.h5\"\n", - "\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True)\n", - "\n", - "sensor_data = kspaceFirstOrder3D(\n", - " medium=medium,\n", - " kgrid=kgrid,\n", - " source=not_transducer,\n", - " sensor=sensor,\n", - " simulation_options=simulation_options,\n", - " execution_options=execution_options,\n", - ")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if USE_STATISTICS:\n", - " fig, axes = plt.subplots(1, 2)\n", - " fig.set_figwidth(8)\n", - " fig.set_figheight(6)\n", - "\n", - " for ind, measures in enumerate(sensor.record):\n", - " im1 = axes[ind].imshow(\n", - " sensor_data[measures].reshape([Nj, Nx]).T * 1e-6,\n", - " extent=[\n", - " min(j_vec * 1e3),\n", - " max(j_vec * 1e3),\n", - " min((kgrid.x_vec - min(kgrid.x_vec)) * 1e3),\n", - " max((kgrid.x_vec - min(kgrid.x_vec)) * 1e3),\n", - " ],\n", - " aspect=\"auto\",\n", - " cmap=\"jet\",\n", - " )\n", - " axes[ind].set_xlabel(\"y-position (mm)\")\n", - " axes[ind].set_ylabel(\"x-position (mm)\")\n", - " title_text = f\"Total Beam Pattern Using {str.upper(measures.split('_')[1])} \\n of Recorded Pressure\"\n", - " axes[ind].set_title(title_text)\n", - " axes[ind].set_yticks(axes[ind].get_yticks().tolist())\n", - " axes[ind].set_yticklabels(axes[ind].get_yticklabels()[::-1])\n", - " axes[ind].set_xlim([-10, 10])\n", - " fig.colorbar(im1, label=\"Pressure [MPa]\", orientation=\"vertical\")\n", - "\n", - " plt.tight_layout()\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if not USE_STATISTICS:\n", - " sensor_data_array = np.reshape(sensor_data[\"p\"], [kgrid.Nt, kgrid.Ny, kgrid.Nx]).transpose(2, 1, 0)\n", - " # compute the amplitude spectrum\n", - " [freq, amp_spect, _] = spect(sensor_data_array, 1 / kgrid.dt, dim=2)\n", - "\n", - " # compute the index at which the source frequency and its harmonics occur\n", - " [f1_value, f1_index] = find_closest(freq, tone_burst_freq)\n", - " [f2_value, f2_index] = find_closest(freq, 2 * tone_burst_freq)\n", - "\n", - " # extract the amplitude at the source frequency and store\n", - " beam_pattern_f1 = np.squeeze(amp_spect[:, :, f1_index])\n", - "\n", - " # extract the amplitude at the second harmonic and store\n", - " beam_pattern_f2 = np.squeeze(amp_spect[:, :, f2_index])\n", - "\n", - " # extract the integral of the total amplitude spectrum\n", - " beam_pattern_total = np.squeeze(np.sum(amp_spect, axis=2))\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if not USE_STATISTICS:\n", - " fig, axes = plt.subplots(1, 3)\n", - " fig.set_figwidth(8)\n", - " fig.set_figheight(6)\n", - "\n", - " for ind, measure in enumerate([beam_pattern_f1, beam_pattern_f2, beam_pattern_total]):\n", - " im1 = axes[ind].imshow(\n", - " np.squeeze(measure) * 1e-6,\n", - " extent=[\n", - " min(j_vec * 1e3),\n", - " max(j_vec * 1e3),\n", - " min((kgrid.x_vec - min(kgrid.x_vec)) * 1e3),\n", - " max((kgrid.x_vec - min(kgrid.x_vec)) * 1e3),\n", - " ],\n", - " aspect=\"auto\",\n", - " cmap=\"jet\",\n", - " )\n", - " axes[ind].set_xlabel(\"y-position (mm)\")\n", - " axes[ind].set_ylabel(\"x-position (mm)\")\n", - "\n", - " axes[ind].set_yticks(axes[ind].get_yticks().tolist())\n", - " axes[ind].set_yticklabels(axes[ind].get_yticklabels()[::-1])\n", - "\n", - " axes[0].set_title(\"Beam Pattern \\nAt Source Fundamental\")\n", - " axes[1].set_title(\"Beam Pattern \\nAt Second Harmonic\")\n", - " axes[2].set_title(\"Total Beam Pattern\\nUsing Integral Of Recorded Pressure\")\n", - "\n", - " plt.tight_layout()\n", - " plt.show()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if not USE_STATISTICS:\n", - " # Compute the directivity at each of the harmonics\n", - " directivity_f1 = beam_pattern_f1[round(not_transducer.focus_distance / dx), :]\n", - " directivity_f2 = beam_pattern_f2[round(not_transducer.focus_distance / dx), :]\n", - "\n", - " # Normalize the directivity\n", - " directivity_f1 /= np.max(directivity_f1)\n", - " directivity_f2 /= np.max(directivity_f2)\n", - "\n", - " # Compute relative angles from the transducer\n", - " if MASK_PLANE == \"xy\":\n", - " horz_axis = ((np.arange(Ny) + 1) - Ny / 2) * dy\n", - " else:\n", - " horz_axis = ((np.arange(Nz) + 1) - Nz / 2) * dz\n", - "\n", - " angles = 180 * np.arctan2(horz_axis, not_transducer.focus_distance) / np.pi\n", - "\n", - " # Plot the directivity\n", - " plt.figure()\n", - " plt.plot(angles, directivity_f1, \"k-\", label=\"Fundamental\")\n", - " plt.plot(angles, directivity_f2, \"k--\", label=\"Second Harmonic\")\n", - " plt.axis(\"tight\")\n", - " plt.xlabel(\"Angle [deg]\")\n", - " plt.ylabel(\"Normalized Amplitude\")\n", - " plt.legend(loc=2)\n", - " plt.xlim([-25, 25])\n", - " plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/us_bmode_linear_transducer/us_bmode_linear_transducer.py b/examples/us_bmode_linear_transducer.py similarity index 88% rename from examples/us_bmode_linear_transducer/us_bmode_linear_transducer.py rename to examples/us_bmode_linear_transducer.py index 4cd1f8251..869b94cd2 100644 --- a/examples/us_bmode_linear_transducer/us_bmode_linear_transducer.py +++ b/examples/us_bmode_linear_transducer.py @@ -1,17 +1,20 @@ +# %% [markdown] +# # Ultrasound B-Mode Linear Transducer Example +# Simulate and process B-mode ultrasound images using a linear transducer array. + +# %% import logging +import os import matplotlib.pyplot as plt import numpy as np import scipy.io -from examples.us_bmode_linear_transducer.example_utils import download_if_does_not_exist from kwave.data import Vector from kwave.kgrid import kWaveGrid from kwave.kmedium import kWaveMedium -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.ktransducer import NotATransducer, kWaveTransducerSimple -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions from kwave.reconstruction.beamform import envelope_detection from kwave.reconstruction.tools import log_compression from kwave.utils.conversion import db2neper @@ -19,12 +22,28 @@ from kwave.utils.filters import gaussian_filter from kwave.utils.signals import get_win, tone_burst + +def _download_from_gdrive(file_id, output_path): + try: + import gdown + except ModuleNotFoundError: + raise AssertionError("This example requires `gdown` to be installed. Please install using `pip install gdown`") + + url = f"https://drive.google.com/uc?export=download&confirm=t&id={file_id}" + gdown.download(url, output_path, quiet=False) + + +def download_if_does_not_exist(file_id, output_path): + if not os.path.exists(output_path): + _download_from_gdrive(file_id, output_path) + + +# %% SENSOR_DATA_GDRIVE_ID = "1lGFTifpOrzBYT4Bl_ccLu_Kx0IDxM0Lv" PHANTOM_DATA_GDRIVE_ID = "1ZfSdJPe8nufZHz0U9IuwHR4chaOGAWO4" PHANTOM_DATA_PATH = "phantom_data.mat" # simulation settings -DATA_CAST = "single" RUN_SIMULATION = False pml_size_points = Vector([20, 10, 10]) # [grid points] @@ -53,6 +72,7 @@ BonA=6, ) +# %% transducer = dotdict() transducer.number_elements = 32 # total number of transducer elements transducer.element_width = 2 # width of each element [grid points/voxels] @@ -80,7 +100,7 @@ not_transducer = NotATransducer(transducer, kgrid, **not_transducer) - +# %% logging.log(logging.INFO, "Fetching phantom data...") download_if_does_not_exist(PHANTOM_DATA_GDRIVE_ID, PHANTOM_DATA_PATH) @@ -99,27 +119,17 @@ medium.sound_speed = sound_speed_map[:, medium_position : medium_position + grid_size_points.y, :] medium.density = density_map[:, medium_position : medium_position + grid_size_points.y, :] - # set the input settings - input_filename = f"example_input_{scan_line_index}.h5" - # set the input settings - simulation_options = SimulationOptions( - pml_inside=False, - pml_size=pml_size_points, - data_cast=DATA_CAST, - data_recast=True, - save_to_disk=True, - input_filename=input_filename, - save_to_disk_exit=False, - ) # run the simulation if RUN_SIMULATION: - sensor_data = kspaceFirstOrder3D( - medium=medium, - kgrid=kgrid, - source=not_transducer, - sensor=not_transducer, - simulation_options=simulation_options, - execution_options=SimulationExecutionOptions(is_gpu_simulation=True), + # NOTE: pml_inside=False not supported in new API + sensor_data = kspaceFirstOrder( + kgrid, + medium, + not_transducer, + not_transducer, + backend="cpp", + device="gpu", + pml_size=(pml_size_points.x, pml_size_points.y, pml_size_points.z), ) scan_lines[scan_line_index, :] = not_transducer.scan_line(not_transducer.combine_sensor_data(sensor_data["p"].T)) @@ -142,7 +152,7 @@ scan_lines = simulation_data - +# %% # ## PROCESS THE RESULTS # ### Remove Input Signal # @@ -218,9 +228,8 @@ scan_lines_fund_log_ex = scan_lines_fund[len(scan_lines_fund) // 2, :] # scan_lines_harm_log_ex = scan_lines_harm[len(scan_lines_harm) // 2, :] - +# %% # ### Visualization -# # Set the desired size of the image @@ -258,10 +267,7 @@ ax.set_ylim(40, 5) plt.tight_layout() - -# In[ ]: - - +# %% # Creating a dictionary with the step labels as keys processing_steps = { "1. Beamformed Signal": scan_lines_no_input, diff --git a/examples/us_bmode_linear_transducer/README.md b/examples/us_bmode_linear_transducer/README.md deleted file mode 100644 index e19a651bd..000000000 --- a/examples/us_bmode_linear_transducer/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Simulating B-mode Ultrasound Images Example - - [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/us_bmode_linear_transducer/us_bmode_linear_transducer.ipynb) - -This example illustrates how k-wave-python can be used for the simulation of B-mode ultrasound images (including tissue harmonic imaging) analogous to those produced by a modern diagnostic ultrasound scanner. - - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_us_bmode_linear_transducer.php). \ No newline at end of file diff --git a/examples/us_bmode_linear_transducer/example_utils.py b/examples/us_bmode_linear_transducer/example_utils.py deleted file mode 100644 index da15cc965..000000000 --- a/examples/us_bmode_linear_transducer/example_utils.py +++ /dev/null @@ -1,16 +0,0 @@ -import os - - -def download_from_gdrive(file_id, output_path): - try: - import gdown - except ModuleNotFoundError: - raise AssertionError("This example requires `gdown` to be installed. Please install using `pip install gdown`") - - url = f"https://drive.google.com/uc?export=download&confirm=t&id={file_id}" - gdown.download(url, output_path, quiet=False) - - -def download_if_does_not_exist(file_id, output_path): - if not os.path.exists(output_path): - download_from_gdrive(file_id, output_path) diff --git a/examples/us_bmode_linear_transducer/us_bmode_linear_transducer.ipynb b/examples/us_bmode_linear_transducer/us_bmode_linear_transducer.ipynb deleted file mode 100644 index 9cfbbfe47..000000000 --- a/examples/us_bmode_linear_transducer/us_bmode_linear_transducer.ipynb +++ /dev/null @@ -1,478 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import importlib.util\n", - "import os\n", - "\n", - "\n", - "def is_running_in_colab()->bool:\n", - " return 'COLAB_RELEASE_TAG' in os.environ\n", - "\n", - "def is_library_installed(library_name):\n", - " spec = importlib.util.find_spec(library_name)\n", - " return spec is not None\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "\n", - "if not is_library_installed('kwave'):\n", - " %pip install k-wave-python\n", - "\n", - "if is_running_in_colab():\n", - " !wget https://raw.githubusercontent.com/waltsims/k-wave-python/master/examples/us_bmode_linear_transducer/example_utils.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import scipy.io\n", - "from example_utils import download_if_does_not_exist\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.ktransducer import NotATransducer, kWaveTransducerSimple\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.reconstruction.beamform import envelope_detection\n", - "from kwave.reconstruction.tools import log_compression\n", - "from kwave.utils.conversion import db2neper\n", - "from kwave.utils.dotdictionary import dotdict\n", - "from kwave.utils.filters import gaussian_filter\n", - "from kwave.utils.signals import get_win, tone_burst\n", - "\n", - "SENSOR_DATA_GDRIVE_ID = '1lGFTifpOrzBYT4Bl_ccLu_Kx0IDxM0Lv'\n", - "PHANTOM_DATA_GDRIVE_ID = '1ZfSdJPe8nufZHz0U9IuwHR4chaOGAWO4'\n", - "PHANTOM_DATA_PATH = 'phantom_data.mat'\n", - "\n", - "# simulation settings\n", - "DATA_CAST = 'single'\n", - "RUN_SIMULATION = True" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pml_size_points = Vector([20, 10, 10]) # [grid points]\n", - "grid_size_points = Vector([256, 128, 128]) - 2 * pml_size_points # [grid points]\n", - "grid_size_meters = 40e-3 # [m]\n", - "grid_spacing_meters = grid_size_meters / Vector([grid_size_points.x, grid_size_points.x, grid_size_points.x])\n", - "\n", - "c0 = 1540\n", - "rho0 = 1000\n", - "source_strength = 1e6 # [Pa]\n", - "tone_burst_freq = 1.5e6 # [Hz]\n", - "tone_burst_cycles = 4\n", - "number_scan_lines = 96" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)\n", - "t_end = (grid_size_points.x * grid_spacing_meters.x) * 2.2 / c0 # [s]\n", - "kgrid.makeTime(c0, t_end=t_end)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "input_signal = tone_burst(1 / kgrid.dt, tone_burst_freq, tone_burst_cycles)\n", - "input_signal = (source_strength / (c0 * rho0)) * input_signal" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "medium = kWaveMedium(\n", - " sound_speed=None, # will be set later\n", - " alpha_coeff=0.75,\n", - " alpha_power=1.5,\n", - " BonA=6\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "transducer = dotdict()\n", - "transducer.number_elements = 32 # total number of transducer elements\n", - "transducer.element_width = 2 # width of each element [grid points/voxels]\n", - "transducer.element_length = 24 # length of each element [grid points/voxels]\n", - "transducer.element_spacing = 0 # spacing (kerf width) between the elements [grid points/voxels]\n", - "transducer.radius = float('inf') # radius of curvature of the transducer [m]\n", - "\n", - "# calculate the width of the transducer in grid points\n", - "transducer_width = transducer.number_elements * transducer.element_width + (\n", - " transducer.number_elements - 1) * transducer.element_spacing\n", - "\n", - "# use this to position the transducer in the middle of the computational grid\n", - "transducer.position = np.round([\n", - " 1,\n", - " grid_size_points.y / 2 - transducer_width / 2,\n", - " grid_size_points.z / 2 - transducer.element_length / 2\n", - "])\n", - "transducer = kWaveTransducerSimple(kgrid, **transducer)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "not_transducer = dotdict()\n", - "not_transducer.sound_speed = c0 # sound speed [m/s]\n", - "not_transducer.focus_distance = 20e-3 # focus distance [m]\n", - "not_transducer.elevation_focus_distance = 19e-3 # focus distance in the elevation plane [m]\n", - "not_transducer.steering_angle = 0 # steering angle [degrees]\n", - "not_transducer.transmit_apodization = 'Hanning'\n", - "not_transducer.receive_apodization = 'Rectangular'\n", - "not_transducer.active_elements = np.ones((transducer.number_elements, 1))\n", - "not_transducer.input_signal = input_signal\n", - "\n", - "not_transducer = NotATransducer(transducer, kgrid, **not_transducer)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "logging.log(logging.INFO, \"Fetching phantom data...\")\n", - "download_if_does_not_exist(PHANTOM_DATA_GDRIVE_ID, PHANTOM_DATA_PATH)\n", - "\n", - "phantom = scipy.io.loadmat(PHANTOM_DATA_PATH)\n", - "sound_speed_map = phantom['sound_speed_map']\n", - "density_map = phantom['density_map']\n", - "\n", - "logging.log(logging.INFO, f\"RUN_SIMULATION set to {RUN_SIMULATION}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# preallocate the storage set medium position\n", - "scan_lines = np.zeros((number_scan_lines, kgrid.Nt))\n", - "medium_position = 0\n", - "\n", - "for scan_line_index in range(0, number_scan_lines):\n", - "\n", - " # load the current section of the medium\n", - " medium.sound_speed = \\\n", - " sound_speed_map[:, medium_position:medium_position + grid_size_points.y, :]\n", - " medium.density = density_map[:, medium_position:medium_position + grid_size_points.y, :]\n", - "\n", - " # set the input settings\n", - " input_filename = f'example_input_{scan_line_index}.h5'\n", - " # set the input settings\n", - " simulation_options = SimulationOptions(\n", - " pml_inside=False,\n", - " pml_size=pml_size_points,\n", - " data_cast=DATA_CAST,\n", - " data_recast=True,\n", - " save_to_disk=True,\n", - " input_filename=input_filename,\n", - " save_to_disk_exit=False\n", - " )\n", - " # run the simulation\n", - " if RUN_SIMULATION:\n", - " sensor_data = kspaceFirstOrder3D(\n", - " medium=medium,\n", - " kgrid=kgrid,\n", - " source=not_transducer,\n", - " sensor=not_transducer,\n", - " simulation_options=simulation_options,\n", - " execution_options=SimulationExecutionOptions(is_gpu_simulation=True)\n", - " )\n", - "\n", - " scan_lines[scan_line_index, :] = not_transducer.scan_line(not_transducer.combine_sensor_data(sensor_data['p'].T))\n", - "\n", - " # update medium position\n", - " medium_position = medium_position + transducer.element_width\n", - "\n", - "if RUN_SIMULATION:\n", - " simulation_data = scan_lines\n", - " scipy.io.savemat('sensor_data.mat', {'sensor_data_all_lines': simulation_data})\n", - "\n", - "else:\n", - " logging.log(logging.INFO, \"Downloading data from remote server...\")\n", - " sensor_data_path = 'sensor_data.mat'\n", - " download_if_does_not_exist(SENSOR_DATA_GDRIVE_ID, sensor_data_path)\n", - "\n", - " simulation_data = scipy.io.loadmat(sensor_data_path)['sensor_data_all_lines']\n", - "\n", - "\n", - "scan_lines = simulation_data" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## PROCESS THE RESULTS\n", - "### Remove Input Signal\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Trim the delay offset from the scan line data\n", - "tukey_win, _ = get_win(kgrid.Nt * 2, 'Tukey', False, 0.05)\n", - "transmit_len = len(input_signal.squeeze())\n", - "scan_line_win = np.concatenate((np.zeros([1, transmit_len * 2]), tukey_win.T[:, :kgrid.Nt - transmit_len * 2]), axis=1)\n", - "\n", - "scan_lines = scan_lines * scan_line_win\n", - "\n", - "# store intermediate results\n", - "scan_lines_no_input = scan_lines[len(scan_lines) // 2, :]\n", - "\n", - "Nt = kgrid.Nt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Time Gain Compensation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create radius variable\n", - "t0 = len(input_signal) * kgrid.dt / 2\n", - "r = c0 * (np.arange(1, Nt + 1) * kgrid.dt - t0) / 2\n", - "\n", - "# Define absorption value and convert to correct units\n", - "tgc_alpha_db_cm = medium.alpha_coeff * (tone_burst_freq * 1e-6)**medium.alpha_power\n", - "tgc_alpha_np_m = db2neper(tgc_alpha_db_cm) * 100\n", - "\n", - "# Create time gain compensation function\n", - "tgc = np.exp(tgc_alpha_np_m * 2 * r)\n", - "\n", - "# Apply the time gain compensation to each of the scan lines\n", - "scan_lines *= tgc\n", - "\n", - "# store intermediate results\n", - "scan_lines_tgc = scan_lines[len(scan_lines) // 2, :]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Frequency Filtering\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scan_lines_fund = gaussian_filter(scan_lines, 1/kgrid.dt, tone_burst_freq, 100)\n", - "scan_lines_harm = gaussian_filter(scan_lines, 1/kgrid.dt, 2 * tone_burst_freq, 30) # plotting was not impl.\n", - "\n", - "# store intermediate results\n", - "scan_lines_fund_ex = scan_lines_fund[len(scan_lines_fund) // 2, :]\n", - "# scan_lines_harm_ex = scan_lines_harm[len(scan_lines_harm) // 2, :]\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Envelope Detection\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scan_lines_fund = envelope_detection(scan_lines_fund)\n", - "scan_lines_harm = envelope_detection(scan_lines_harm)\n", - "\n", - "# store intermediate results\n", - "scan_lines_fund_env_ex = scan_lines_fund[len(scan_lines_fund) // 2, :]\n", - "# scan_lines_harm_env_ex = scan_lines_harm[len(scan_lines_harm) // 2, :]\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Log Compression\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "compression_ratio = 3\n", - "scan_lines_fund = log_compression(scan_lines_fund, compression_ratio, True)\n", - "scan_lines_harm = log_compression(scan_lines_harm, compression_ratio, True)\n", - "\n", - "# store intermediate results\n", - "scan_lines_fund_log_ex = scan_lines_fund[len(scan_lines_fund) // 2, :]\n", - "# scan_lines_harm_log_ex = scan_lines_harm[len(scan_lines_harm) // 2, :]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Visualization\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Set the desired size of the image\n", - "image_size = kgrid.size\n", - "\n", - "# Create the axis variables\n", - "x_axis = [0, image_size[0] * 1e3 * 1.1] # [mm]\n", - "y_axis = [-0.5 * image_size[1] * 1e3, 0.5 * image_size[1] * 1e3] # [mm]\n", - "\n", - "# make plotting non-blocking\n", - "plt.ion()\n", - "# Plot the data before and after scan conversion\n", - "plt.figure(figsize=(14, 4))\n", - "# plot the sound speed map\n", - "plt.subplot(1, 3, 1)\n", - "plt.imshow(sound_speed_map[:, 64:-64, int(grid_size_points.z / 2)], aspect='auto',\n", - " extent=[y_axis[0], y_axis[1], x_axis[1], x_axis[0]])\n", - "plt.title('Sound Speed')\n", - "plt.xlabel('Width [mm]')\n", - "plt.ylabel('Depth [mm]')\n", - "ax = plt.gca()\n", - "ax.set_ylim(40, 5)\n", - "plt.subplot(1, 3, 2)\n", - "plt.imshow(scan_lines_fund.T, cmap='gray', aspect='auto', extent=[y_axis[0], y_axis[1], x_axis[1], x_axis[0]])\n", - "plt.xlabel('Image width [mm]')\n", - "plt.title('Fundamental')\n", - "ax = plt.gca()\n", - "ax.set_ylim(40, 5)\n", - "plt.yticks([])\n", - "plt.subplot(1, 3, 3)\n", - "plt.imshow(scan_lines_harm.T, cmap='gray', aspect='auto', extent=[y_axis[0], y_axis[1], x_axis[1], x_axis[0]])\n", - "plt.yticks([])\n", - "plt.xlabel('Image width [mm]')\n", - "plt.title('Harmonic')\n", - "ax = plt.gca()\n", - "ax.set_ylim(40, 5)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Creating a dictionary with the step labels as keys\n", - "processing_steps = {\n", - " '1. Beamformed Signal': scan_lines_no_input,\n", - " '2. Time Gain Compensation': scan_lines_tgc,\n", - " '3. Frequency Filtering': scan_lines_fund_ex,\n", - " '4. Envelope Detection': scan_lines_fund_env_ex,\n", - " '5. Log Compression': scan_lines_fund_log_ex\n", - "}\n", - "\n", - "plt.figure(figsize=(14, 4), tight_layout=True)\n", - "\n", - "offset = -6e5\n", - "# Plotting each step using the dictionary\n", - "for i, (label, data) in enumerate(processing_steps.items()):\n", - " plt.plot(kgrid.t_array.squeeze(), data.squeeze() + offset * i, label=label)\n", - "\n", - "# Set y-ticks and y-labels\n", - "plt.yticks([offset * i for i in range(5)], list(processing_steps.keys()))\n", - "plt.xlabel('Time [\\u03BCs]')\n", - "plt.xlim(5e-3 * 2 / c0, t_end)\n", - "plt.title('Processing Steps Visualization')\n", - "plt.show()\n" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/examples/us_bmode_phased_array/us_bmode_phased_array.py b/examples/us_bmode_phased_array.py similarity index 88% rename from examples/us_bmode_phased_array/us_bmode_phased_array.py rename to examples/us_bmode_phased_array.py index 823d17964..0c877c605 100644 --- a/examples/us_bmode_phased_array/us_bmode_phased_array.py +++ b/examples/us_bmode_phased_array.py @@ -1,5 +1,8 @@ -from copy import deepcopy +# %% [markdown] +# # Ultrasound B-Mode Phased Array Example +# Simulate and process B-mode ultrasound images using a phased array transducer with steering. +# %% import numpy as np import scipy.io from matplotlib import pyplot as plt @@ -7,10 +10,8 @@ from kwave.data import Vector from kwave.kgrid import kWaveGrid from kwave.kmedium import kWaveMedium -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.ktransducer import NotATransducer, kWaveTransducerSimple -from kwave.options.simulation_execution_options import SimulationExecutionOptions -from kwave.options.simulation_options import SimulationOptions from kwave.reconstruction.beamform import envelope_detection, scan_conversion from kwave.reconstruction.tools import log_compression from kwave.utils.conversion import db2neper @@ -19,8 +20,8 @@ from kwave.utils.mapgen import make_ball from kwave.utils.signals import get_win, tone_burst +# %% # simulation settings -DATA_CAST = "single" RUN_SIMULATION = True @@ -44,10 +45,7 @@ t_end = (grid_size_points.x * grid_spacing_meters.x) * 2.2 / c0 # [s] kgrid.makeTime(c0, t_end=t_end) - -# In[ ]: - - +# %% source_strength = 1e6 # [Pa] tone_burst_freq = 1e6 # [Hz] tone_burst_cycles = 4 @@ -83,7 +81,7 @@ not_transducer = NotATransducer(transducer, kgrid, **not_transducer) - +# %% # Define a random distribution of scatterers for the medium background_map_mean = 1 background_map_std = 0.008 @@ -113,7 +111,7 @@ medium.sound_speed = sound_speed_map medium.density = density_map - +# %% # Range of steering angles to test steering_angles = np.arange(-32, 33, 2) @@ -125,29 +123,18 @@ for angle_index in range(number_scan_lines): print(f"Computing scan line {angle_index} of {number_scan_lines}") - # set the input settings - input_filename = f"example_input_{angle_index}.h5" - # set the input settings - simulation_options = SimulationOptions( - pml_inside=False, - pml_size=pml_size_points, - data_cast=DATA_CAST, - data_recast=True, - save_to_disk=True, - input_filename=input_filename, - save_to_disk_exit=False, - ) - # Update the current steering angle not_transducer.steering_angle = steering_angles[angle_index] - sensor_data = kspaceFirstOrder3D( - medium=deepcopy(medium), # Medium is altered in-place in this function - kgrid=kgrid, - source=not_transducer, - sensor=not_transducer, - simulation_options=simulation_options, - execution_options=SimulationExecutionOptions(is_gpu_simulation=True), + # NOTE: pml_inside=False not supported in new API + sensor_data = kspaceFirstOrder( + kgrid, + medium, + not_transducer, + not_transducer, + backend="cpp", + device="gpu", + pml_size=(pml_size_points.x, pml_size_points.y, pml_size_points.z), ) scan_lines[angle_index, :] = not_transducer.scan_line(not_transducer.combine_sensor_data(sensor_data["p"].T)) @@ -156,7 +143,7 @@ else: scan_lines = scipy.io.loadmat("example_us_phased_array_scan_lines")["scan_lines"] - +# %% # PROCESS THE RESULTS # Remove Input Signal # Trim the delay offset from the scan line data @@ -204,14 +191,11 @@ # Log Compression -# In[ ]: - - compression_ratio = 3 scan_lines_fund = log_compression(scan_lines_fund, compression_ratio, True) scan_lines_harm = log_compression(scan_lines_harm, compression_ratio, True) - +# %% # Visualization image_size = [kgrid.Nx * kgrid.dx, kgrid.Ny * kgrid.dy] image_res = [256, 256] diff --git a/examples/us_bmode_phased_array/README.md b/examples/us_bmode_phased_array/README.md deleted file mode 100644 index 528b75f38..000000000 --- a/examples/us_bmode_phased_array/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Simulating B-mode Ultrasound Images From a Phased Array Transducer Example - - [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/us_bmode_phased_array/us_bmode_phased_array.ipynb) - -This example illustrates how k-wave-python can be used for the simulation of B-mode ultrasound images (including tissue harmonic imaging) analogous to those produced by a modern diagnostic ultrasound scanner. -Specifically, it demonstrates the usage of a phased array transducer. In this case, the entire medium should fit into the kgrid. - - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_us_bmode_phased_array.php). \ No newline at end of file diff --git a/examples/us_bmode_phased_array/us_bmode_phased_array.ipynb b/examples/us_bmode_phased_array/us_bmode_phased_array.ipynb deleted file mode 100644 index 23279f1e2..000000000 --- a/examples/us_bmode_phased_array/us_bmode_phased_array.ipynb +++ /dev/null @@ -1,468 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install git+https://github.com/waltsims/k-wave-python" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from copy import deepcopy\n", - "\n", - "import numpy as np\n", - "import scipy.io\n", - "from matplotlib import pyplot as plt\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.ktransducer import NotATransducer, kWaveTransducerSimple\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.options.simulation_options import SimulationOptions\n", - "from kwave.reconstruction.beamform import envelope_detection, scan_conversion\n", - "from kwave.reconstruction.tools import log_compression\n", - "from kwave.utils.conversion import db2neper\n", - "from kwave.utils.dotdictionary import dotdict\n", - "from kwave.utils.filters import gaussian_filter\n", - "from kwave.utils.mapgen import make_ball\n", - "from kwave.utils.signals import get_win, tone_burst" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# simulation settings\n", - "DATA_CAST = 'single'\n", - "RUN_SIMULATION = True" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pml_size_points = Vector([15, 10, 10]) # [grid points]\n", - "grid_size_points = Vector([256, 256, 128]) - 2 * pml_size_points # [grid points]\n", - "grid_size_meters = 50e-3 # [m]\n", - "grid_spacing_meters = grid_size_meters / Vector([grid_size_points.x, grid_size_points.x, grid_size_points.x])\n", - "\n", - "c0 = 1540\n", - "rho0 = 1000\n", - "\n", - "medium = kWaveMedium(\n", - " sound_speed=None, # will be set later\n", - " alpha_coeff=0.75,\n", - " alpha_power=1.5,\n", - " BonA=6\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)\n", - "t_end = (grid_size_points.x * grid_spacing_meters.x) * 2.2 / c0 # [s]\n", - "kgrid.makeTime(c0, t_end=t_end)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "source_strength = 1e6 # [Pa]\n", - "tone_burst_freq = 1e6 # [Hz]\n", - "tone_burst_cycles = 4\n", - "\n", - "input_signal = tone_burst(1 / kgrid.dt, tone_burst_freq, tone_burst_cycles)\n", - "input_signal = (source_strength / (c0 * rho0)) * input_signal" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "transducer = dotdict()\n", - "transducer.number_elements = 64 # total number of transducer elements\n", - "transducer.element_width = 1 # width of each element [grid points/voxels]\n", - "transducer.element_length = 40 # length of each element [grid points/voxels]\n", - "transducer.element_spacing = 0 # spacing (kerf width) between the elements [grid points/voxels]\n", - "transducer.radius = float('inf') # radius of curvature of the transducer [m]\n", - "\n", - "# calculate the width of the transducer in grid points\n", - "transducer_width = transducer.number_elements * transducer.element_width + (\n", - " transducer.number_elements - 1) * transducer.element_spacing\n", - "\n", - "# use this to position the transducer in the middle of the computational grid\n", - "transducer.position = np.round([\n", - " 1,\n", - " grid_size_points.y / 2 - transducer_width / 2,\n", - " grid_size_points.z / 2 - transducer.element_length / 2\n", - "])\n", - "transducer = kWaveTransducerSimple(kgrid, **transducer)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "not_transducer = dotdict()\n", - "not_transducer.sound_speed = c0 # sound speed [m/s]\n", - "not_transducer.focus_distance = 30e-3 # focus distance [m]\n", - "not_transducer.elevation_focus_distance = 30e-3 # focus distance in the elevation plane [m]\n", - "not_transducer.steering_angle = 0 # steering angle [degrees]\n", - "not_transducer.steering_angle_max = 32 # steering angle [degrees]\n", - "not_transducer.transmit_apodization = 'Hanning'\n", - "not_transducer.receive_apodization = 'Rectangular'\n", - "not_transducer.active_elements = np.ones((transducer.number_elements, 1))\n", - "not_transducer.input_signal = input_signal\n", - "\n", - "not_transducer = NotATransducer(transducer, kgrid, **not_transducer)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define a random distribution of scatterers for the medium\n", - "background_map_mean = 1\n", - "background_map_std = 0.008\n", - "background_map = background_map_mean + background_map_std * np.random.randn(kgrid.Nx, kgrid.Ny, kgrid.Nz)\n", - "\n", - "sound_speed_map = c0 * background_map\n", - "density_map = rho0 * background_map" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define a random distribution of scatterers for the highly scattering region\n", - "scattering_map = np.random.randn(kgrid.Nx, kgrid.Ny, kgrid.Nz)\n", - "scattering_c0 = np.clip(c0 + 25 + 75 * scattering_map, 1400, 1600)\n", - "scattering_rho0 = scattering_c0 / 1.5" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define a sphere for a highly scattering region\n", - "radius = 8e-3\n", - "x_pos = 32e-3\n", - "y_pos = kgrid.dy * kgrid.Ny / 2\n", - "z_pos = kgrid.dz * kgrid.Nz / 2\n", - "ball_center = np.round(Vector([x_pos, y_pos, z_pos]) / kgrid.dx)\n", - "scattering_region1 = make_ball(grid_size_points, ball_center, round(radius / kgrid.dx)).nonzero()\n", - "\n", - "sound_speed_map[scattering_region1] = scattering_c0[scattering_region1]\n", - "density_map[scattering_region1] = scattering_rho0[scattering_region1]\n", - "\n", - "medium.sound_speed = sound_speed_map\n", - "medium.density = density_map" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Range of steering angles to test\n", - "steering_angles = np.arange(-32, 33, 2)\n", - "\n", - "# Preallocate the storage\n", - "number_scan_lines = len(steering_angles)\n", - "scan_lines = np.zeros((number_scan_lines, kgrid.Nt))\n", - "\n", - "if RUN_SIMULATION:\n", - " for angle_index in range(number_scan_lines):\n", - " print(f'Computing scan line {angle_index} of {number_scan_lines}')\n", - "\n", - " # set the input settings\n", - " input_filename = f'example_input_{angle_index}.h5'\n", - " # set the input settings\n", - " simulation_options = SimulationOptions(\n", - " pml_inside=False,\n", - " pml_size=pml_size_points,\n", - " data_cast=DATA_CAST,\n", - " data_recast=True,\n", - " save_to_disk=True,\n", - " input_filename=input_filename,\n", - " save_to_disk_exit=False\n", - " )\n", - "\n", - " # Update the current steering angle\n", - " not_transducer.steering_angle = steering_angles[angle_index]\n", - "\n", - " sensor_data = kspaceFirstOrder3D(\n", - " medium=deepcopy(medium), # Medium is altered in-place in this function\n", - " kgrid=kgrid,\n", - " source=not_transducer,\n", - " sensor=not_transducer,\n", - " simulation_options=simulation_options,\n", - " execution_options=SimulationExecutionOptions(is_gpu_simulation=True)\n", - " )\n", - "\n", - " scan_lines[angle_index, :] = not_transducer.scan_line(not_transducer.combine_sensor_data(sensor_data['p'].T))\n", - "\n", - " scipy.io.savemat('example_us_phased_array_scan_lines.mat', {'scan_lines': scan_lines})\n", - "else:\n", - " scan_lines = scipy.io.loadmat('example_us_phased_array_scan_lines')['scan_lines']\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## PROCESS THE RESULTS\n", - "### Remove Input Signal" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Trim the delay offset from the scan line data\n", - "t0_offset = int(round(len(input_signal.squeeze())/ 2) + (not_transducer.appended_zeros - not_transducer.beamforming_delays_offset)) \n", - "\n", - "scan_lines = scan_lines[:, t0_offset:]\n", - "\n", - "Nt = np.shape(scan_lines)[1]\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "tukey_win, _ = get_win(Nt * 2, 'Tukey', False, 0.05)\n", - "scan_line_win = np.concatenate((np.zeros([1, t0_offset * 2]), tukey_win.T[:, :int(len(tukey_win)/2) - t0_offset * 2]), axis=1)\n", - "\n", - "scan_lines = scan_lines * scan_line_win\n", - "# store intermediate results" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Time Gain Compensation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create radius variable\n", - "r = c0 * np.arange(1, Nt + 1) * kgrid.dt / 2\n", - "\n", - "# Define absorption value and convert to correct units\n", - "tgc_alpha_db_cm = medium.alpha_coeff * (tone_burst_freq * 1e-6)**medium.alpha_power\n", - "tgc_alpha_np_m = db2neper(tgc_alpha_db_cm) * 100\n", - "\n", - "# Create time gain compensation function\n", - "tgc = np.exp(tgc_alpha_np_m * 2 * r)\n", - "\n", - "# Apply the time gain compensation to each of the scan lines\n", - "scan_lines *= tgc" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Frequency Filtering" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scan_lines_fund = gaussian_filter(scan_lines, 1/kgrid.dt, tone_burst_freq, 100)\n", - "scan_lines_harm = gaussian_filter(scan_lines, 1/kgrid.dt, 2 * tone_burst_freq, 30)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Envelope Detection" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scan_lines_fund = envelope_detection(scan_lines_fund)\n", - "scan_lines_harm = envelope_detection(scan_lines_harm)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Log Compression" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "compression_ratio = 3\n", - "scan_lines_fund = log_compression(scan_lines_fund, compression_ratio, True)\n", - "scan_lines_harm = log_compression(scan_lines_harm, compression_ratio, True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Visualization" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "image_size = [kgrid.Nx * kgrid.dx, kgrid.Ny * kgrid.dy]\n", - "image_res = [256, 256]\n", - "\n", - "b_mode_fund = scan_conversion(scan_lines_fund, steering_angles, image_size, c0, kgrid.dt, image_res)\n", - "b_mode_harm = scan_conversion(scan_lines_harm, steering_angles, image_size, c0, kgrid.dt, image_res)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create the axis variables\n", - "x_axis = [0, image_size[0] * 1e3] # [mm]\n", - "y_axis = [0, image_size[1] * 1e3] # [mm]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.ion()\n", - "plt.figure(figsize=(15, 4))\n", - "plt.subplot(131)\n", - "plt.imshow(scan_lines.T, aspect='auto',\n", - " extent=[steering_angles[-1], steering_angles[0],y_axis[1], y_axis[0] ], interpolation='none', cmap='gray')\n", - "plt.xlabel('Steering angle [deg]')\n", - "plt.ylabel('Depth [mm]')\n", - "plt.title('Raw Scan-Line Data')\n", - "\n", - "\n", - "plt.subplot(132)\n", - "plt.imshow(scan_lines_fund.T, aspect='auto',\n", - " extent=[steering_angles[-1], steering_angles[0],y_axis[1], y_axis[0] ], interpolation='none', cmap='bone')\n", - "plt.xlabel('Steering angle [deg]')\n", - "plt.ylabel('Depth [mm]')\n", - "plt.title('Processed Scan-Line Data')\n", - "\n", - "plt.subplot(133)\n", - "plt.imshow(b_mode_fund, cmap='bone', aspect='auto', extent=[y_axis[0], y_axis[1], x_axis[1], x_axis[0]], interpolation='none')\n", - "plt.xlabel('Horizontal Position [mm]')\n", - "plt.ylabel('Depth [mm]')\n", - "plt.title('B-Mode Image')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure(figsize=(15, 4))\n", - "plt.subplot(131)\n", - "plt.imshow(medium.sound_speed[..., kgrid.Nz//2], aspect='auto',\n", - " extent=[y_axis[0], y_axis[1], x_axis[1], x_axis[0]])\n", - "plt.xlabel('Horizontal Position [mm]')\n", - "plt.ylabel('Depth [mm]')\n", - "plt.title('Scattering Phantom')\n", - "\n", - "plt.subplot(132)\n", - "plt.imshow(b_mode_fund, cmap='bone', aspect='auto', extent=[y_axis[0], y_axis[1], x_axis[1], x_axis[0]], interpolation='none')\n", - "plt.xlabel('Horizontal Position [mm]')\n", - "plt.ylabel('Depth [mm]')\n", - "plt.title('B-Mode Image')\n", - "\n", - "plt.subplot(133)\n", - "plt.imshow(b_mode_harm, cmap='bone', aspect='auto', extent=[y_axis[0], y_axis[1], x_axis[1], x_axis[0]], interpolation='none')\n", - "plt.xlabel('Horizontal Position [mm]')\n", - "plt.ylabel('Depth [mm]')\n", - "plt.title('Harmonic Image')\n", - "\n", - "plt.show()\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "deep-us", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/us_defining_transducer/us_defining_transducer.py b/examples/us_defining_transducer.py similarity index 86% rename from examples/us_defining_transducer/us_defining_transducer.py rename to examples/us_defining_transducer.py index 74373e83d..043ef2b99 100644 --- a/examples/us_defining_transducer/us_defining_transducer.py +++ b/examples/us_defining_transducer.py @@ -1,22 +1,22 @@ +# %% [markdown] +# # Defining An Ultrasound Transducer Example +# Define a transducer array, run a simulation, and analyze the recorded signals and spectra. + +# %% import matplotlib.pyplot as plt import numpy as np -from kwave.data import Vector from kwave.kgrid import kWaveGrid from kwave.kmedium import kWaveMedium from kwave.ksensor import kSensor -from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D +from kwave.kspaceFirstOrder import kspaceFirstOrder from kwave.ktransducer import NotATransducer, kWaveTransducerSimple -from kwave.kWaveSimulation import SimulationOptions -from kwave.options.simulation_execution_options import SimulationExecutionOptions from kwave.utils.dotdictionary import dotdict from kwave.utils.filters import spect from kwave.utils.plot import voxel_plot from kwave.utils.signals import tone_burst -# simulation settings -DATA_CAST = "single" - +# %% # define the grid PML_X_SIZE = 20 PML_Y_SIZE = 10 @@ -45,6 +45,7 @@ input_signal = tone_burst(1 / kgrid.dt, tone_burst_freq, tone_burst_cycles) input_signal = (source_strength / (medium.sound_speed * medium.density)) * input_signal +# %% # define the transducer transducer = dotdict() transducer.number_elements = 72 @@ -72,6 +73,7 @@ voxel_plot(np.single(not_transducer.all_elements_mask)) +# %% # define sensor mask sensor_mask = np.zeros((Nx, Ny, Nz)) sensor_mask[Nx // 4, Ny // 2, Nz // 2] = 1 @@ -81,24 +83,18 @@ sensor.record = ["p"] # SIMULATION -simulation_options = SimulationOptions( - pml_inside=False, - pml_size=Vector([PML_X_SIZE, PML_Y_SIZE, PML_Z_SIZE]), - data_cast=DATA_CAST, - save_to_disk=True, -) - -execution_options = SimulationExecutionOptions(is_gpu_simulation=True) - -sensor_data = kspaceFirstOrder3D( - medium=medium, - kgrid=kgrid, - source=not_transducer, - sensor=sensor, - simulation_options=simulation_options, - execution_options=execution_options, +# NOTE: pml_inside=False not supported in new API +sensor_data = kspaceFirstOrder( + kgrid, + medium, + not_transducer, + sensor, + backend="cpp", + device="gpu", + pml_size=(PML_X_SIZE, PML_Y_SIZE, PML_Z_SIZE), ) +# %% padded_input_signal = np.concatenate((input_signal, np.zeros((1, 2 * np.shape(input_signal)[1]))), axis=1) f_input, as_input, _ = spect(padded_input_signal, 1 / kgrid.dt) _, as_1, _ = spect(sensor_data["p"][:, 0], 1 / kgrid.dt) diff --git a/examples/us_defining_transducer/README.md b/examples/us_defining_transducer/README.md deleted file mode 100644 index a56ec5664..000000000 --- a/examples/us_defining_transducer/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Defining An Ultrasound Transducer Example - - [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/waltsims/k-wave-python/blob/HEAD/examples/us_defining_transducer/us_defining_transducer.ipynb) - -This example illustrates how kWaveTransducerSimple can be used to simulate the field produced by an ultrasound transducer. - - -To read more, visit the [original example page](http://www.k-wave.org/documentation/example_us_defining_transducer.php). diff --git a/examples/us_defining_transducer/us_defining_transducer.ipynb b/examples/us_defining_transducer/us_defining_transducer.ipynb deleted file mode 100644 index b98b6f132..000000000 --- a/examples/us_defining_transducer/us_defining_transducer.ipynb +++ /dev/null @@ -1,271 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install k-wave-python" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "from kwave.data import Vector\n", - "from kwave.kgrid import kWaveGrid\n", - "from kwave.kmedium import kWaveMedium\n", - "from kwave.ksensor import kSensor\n", - "from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D\n", - "from kwave.ktransducer import NotATransducer, kWaveTransducerSimple\n", - "from kwave.kWaveSimulation import SimulationOptions\n", - "from kwave.options.simulation_execution_options import SimulationExecutionOptions\n", - "from kwave.utils.dotdictionary import dotdict\n", - "from kwave.utils.filters import spect\n", - "from kwave.utils.plot import voxel_plot\n", - "from kwave.utils.signals import tone_burst" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# simulation settings\n", - "DATA_CAST = 'single'\n", - "\n", - "# define the grid\n", - "PML_X_SIZE = 20\n", - "PML_Y_SIZE = 10\n", - "PML_Z_SIZE = 10\n", - "Nx = 128 - 2*PML_X_SIZE\n", - "Ny = 128 - 2*PML_Y_SIZE\n", - "Nz = 64 - 2*PML_Z_SIZE\n", - "x = 40e-3\n", - "dx = x/Nx\n", - "dy = dx\n", - "dz = dx\n", - "\n", - "kgrid = kWaveGrid([Nx, Ny, Nz], [dx, dy, dz])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "# define the medium\n", - "medium = kWaveMedium(sound_speed=1540,\n", - " density=1000,\n", - " alpha_coeff=0.75,\n", - " alpha_power=1.5,\n", - " BonA=6)\n", - "\n", - "# create the time array\n", - "t_end = 40e-6\n", - "kgrid.makeTime(medium.sound_speed, t_end=t_end)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define the input signal\n", - "source_strength = 1e6\n", - "tone_burst_freq = 0.5e6\n", - "tone_burst_cycles = 5\n", - "input_signal = tone_burst(1/kgrid.dt, tone_burst_freq, tone_burst_cycles)\n", - "input_signal = (source_strength/ (medium.sound_speed * medium.density)) * input_signal\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define the transducer\n", - "transducer = dotdict()\n", - "transducer.number_elements = 72\n", - "transducer.element_width = 1\n", - "transducer.element_length = 12\n", - "transducer.element_spacing = 0\n", - "transducer.radius = np.inf\n", - "\n", - "# calculate the width of the transducer in grid points\n", - "transducer_width = transducer.number_elements * transducer.element_width + (transducer.number_elements - 1) * transducer.element_spacing\n", - "\n", - "# use this to position the transducer in the middle of the computational grid\n", - "transducer.position = np.round([1, Ny / 2 - transducer_width / 2, Nz / 2 - transducer.element_length / 2])\n", - "transducer = kWaveTransducerSimple(kgrid, **transducer)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "not_transducer = dotdict()\n", - "not_transducer.sound_speed = medium.sound_speed # sound speed [m/s]\n", - "not_transducer.focus_distance = 20e-3 # focus distance [m]\n", - "not_transducer.elevation_focus_distance = 19e-3 # focus distance in the elevation plane [m]\n", - "not_transducer.steering_angle = 0 # steering angle [degrees]\n", - "not_transducer.transmit_apodization = \"Rectangular\"\n", - "not_transducer.receive_apodization = \"Rectangular\"\n", - "not_transducer.active_elements = np.zeros((transducer.number_elements, 1))\n", - "not_transducer.active_elements[21:52] = 1\n", - "not_transducer.input_signal = input_signal\n", - "\n", - "not_transducer = NotATransducer(transducer, kgrid, **not_transducer)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "voxel_plot(np.single(not_transducer.all_elements_mask))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# define sensor mask\n", - "sensor_mask = np.zeros((Nx, Ny, Nz))\n", - "sensor_mask[Nx//4, Ny//2, Nz//2] = 1\n", - "sensor_mask[Nx//2, Ny//2, Nz//2] = 1\n", - "sensor_mask[3*Nx//4, Ny//2, Nz//2] = 1\n", - "sensor = kSensor(sensor_mask)\n", - "sensor.record=['p']\n", - "\n", - "# SIMULATION\n", - "simulation_options = SimulationOptions(\n", - " pml_inside=False,\n", - " pml_size=Vector([PML_X_SIZE, PML_Y_SIZE, PML_Z_SIZE]),\n", - " data_cast=DATA_CAST, \n", - " save_to_disk=True,\n", - ")\n", - "\n", - "execution_options = SimulationExecutionOptions(is_gpu_simulation=True)\n", - "\n", - "sensor_data = kspaceFirstOrder3D(medium=medium, kgrid=kgrid, source=not_transducer, sensor=sensor,\n", - " simulation_options=simulation_options, execution_options=execution_options)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "padded_input_signal = np.concatenate((input_signal, np.zeros((1, 2 * np.shape(input_signal)[1]))), axis=1)\n", - "f_input, as_input, _ = spect(padded_input_signal, 1/kgrid.dt)\n", - "_, as_1, _ = spect(sensor_data['p'][:, 0], 1/kgrid.dt)\n", - "_, as_2, _ = spect(sensor_data['p'][:, 1], 1/kgrid.dt)\n", - "f, as_3, _ = spect(sensor_data['p'][:, 2], 1/kgrid.dt)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig, axes = plt.subplots(2,1)\n", - "axes[0].plot(np.arange(0,input_signal.shape[-1]) * kgrid.dt * 1e6, input_signal.T, 'k-')\n", - "axes[0].set_xlabel('Time [\\mus]')\n", - "axes[0].set_ylabel('Particle Velocity [m/s]')\n", - "\n", - "axes[1].plot(f_input/1e6, np.squeeze(as_input/np.max(as_input)), 'k-')\n", - "axes[1].plot([tone_burst_freq/1e6, tone_burst_freq/1e6], [0, 1], 'k--')\n", - "axes[1].set_xlabel('Frequency [MHz]')\n", - "axes[1].set_ylabel('Amplitude Spectrum [au]')\n", - "f_max = medium.sound_speed / (2*dx)\n", - "axes[1].set_xlim([0, f_max/1e6])\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Creating a dictionary with the step labels as keys\n", - "sensor_positions = {\n", - " 'Sensor Position 1': sensor_data['p'][:,0],\n", - " 'Sensor Position 2': sensor_data['p'][:,1],\n", - " 'Sensor Position 3': sensor_data['p'][:,2]\n", - "}\n", - "\n", - "fig, axes = plt.subplots(1,2)\n", - "fig.set_figwidth(16)\n", - "fig.tight_layout = True\n", - "\n", - "offset = -30e5\n", - "# Plotting each step using the dictionary\n", - "for i, (label, data) in enumerate(sensor_positions.items()):\n", - " axes[0].plot(kgrid.t_array.squeeze()[:len(data.squeeze())] * 1e6, data.squeeze() + offset * i, label=label)\n", - "\n", - "# Set y-ticks and y-labels\n", - "axes[0].set_yticks([offset * i for i in range(len(sensor_positions.keys()))], list(sensor_positions.keys()))\n", - "axes[0].set_xlabel('Time [\\u03BCs]')\n", - "\n", - "\n", - "axes[1].plot(f * 1e-6, as_1 / np.max(as_1.flatten()), 'k-', label = 'Sensor Position 1')\n", - "axes[1].plot(f * 1e-6, as_2 / np.max(as_1.flatten()), 'b-', label = 'Sensor Position 2')\n", - "axes[1].plot(f * 1e-6, as_3 / np.max(as_1.flatten()), 'r-', label = 'Sensor Position 3')\n", - "axes[1].legend()\n", - "axes[1].set_xlabel('Frequency [MHz]')\n", - "axes[1].set_ylabel('Normalised Amplitude Spectrum [au]')\n", - "f_max = medium.sound_speed / (2 * dx)\n", - "axes[1].set_xlim([0, f_max * 1e-6])\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/jupytext.toml b/jupytext.toml new file mode 100644 index 000000000..718896859 --- /dev/null +++ b/jupytext.toml @@ -0,0 +1,3 @@ +# Jupytext config: .py files with # %% markers are the source of truth. +# Generate notebooks with: jupytext --to notebook examples/**/*.py +formats = "py:percent" diff --git a/kwave/kspaceFirstOrder.py b/kwave/kspaceFirstOrder.py index 2b587ecde..876bd4f9f 100644 --- a/kwave/kspaceFirstOrder.py +++ b/kwave/kspaceFirstOrder.py @@ -1,8 +1,10 @@ import copy +import os import warnings from typing import Optional, Union import numpy as np +from beartype import beartype as typechecker from kwave.kgrid import kWaveGrid from kwave.kmedium import kWaveMedium @@ -80,10 +82,11 @@ def _strip_pml(result, pml_size, ndim): } +@typechecker def kspaceFirstOrder( kgrid: kWaveGrid, medium: kWaveMedium, - source: kSource, + source: Union[kSource, NotATransducer], sensor: Union[kSensor, NotATransducer, None] = None, *, pml_size: Union[int, tuple, str] = 20, @@ -156,7 +159,15 @@ def kspaceFirstOrder( Returns: dict: Recorded sensor data keyed by field name (e.g. ``"p"``, ``"p_final"``, ``"ux"``, ``"uy"``). + + Environment variables: + ``KWAVE_BACKEND``: Override ``backend`` (e.g. ``"python"``). + ``KWAVE_DEVICE``: Override ``device`` (e.g. ``"cpu"``). """ + # Environment overrides — lets CI run GPU/cpp examples on CPU/python + backend = os.environ.get("KWAVE_BACKEND", backend) + device = os.environ.get("KWAVE_DEVICE", device) + if device not in ("cpu", "gpu"): raise ValueError(f"device must be 'cpu' or 'gpu', got {device!r}") if backend not in ("python", "cpp"): @@ -173,6 +184,14 @@ def kspaceFirstOrder( # --- Shared pre-processing (both backends) --- + # Expand grid when PML sits outside the user domain + if pml_inside: + warnings.warn( + f"pml_inside=True: the outermost {pml_size} grid points per side will be used for PML absorption. " + "Sources, sensors, and medium properties near the boundary will be affected. " + "Set pml_inside=False (default) to expand the grid automatically instead.", + stacklevel=2, + ) if not pml_inside: kgrid, medium, source, sensor = _expand_for_pml_outside(kgrid, medium, source, sensor, pml_size) @@ -181,7 +200,8 @@ def kspaceFirstOrder( from kwave.utils.filters import smooth source = copy.copy(source) - source.p0 = smooth(np.asarray(source.p0, dtype=float).reshape(tuple(int(n) for n in kgrid.N), order="F"), restore_max=True) + grid_shape = tuple(int(n) for n in kgrid.N) + source.p0 = smooth(np.asarray(source.p0, dtype=float).reshape(grid_shape, order="F"), restore_max=True) # --- Backend dispatch --- @@ -196,7 +216,7 @@ def kspaceFirstOrder( device=device, use_sg=use_sg, use_kspace=use_kspace, - smooth_p0=False, + smooth_p0=False, # already handled above pml_size=pml_size, pml_alpha=pml_alpha, ).run() diff --git a/kwave/solvers/validation.py b/kwave/solvers/validation.py index a628b1a16..38f53a73f 100644 --- a/kwave/solvers/validation.py +++ b/kwave/solvers/validation.py @@ -72,21 +72,21 @@ def validate_cfl(kgrid, medium): def validate_source(source, kgrid): grid_size = int(np.prod(kgrid.N)) - if source.p0 is not None: + if getattr(source, "p0", None) is not None: p0 = np.asarray(source.p0) if p0.size != grid_size and p0.size != 0: raise ValueError(f"source.p0 has {p0.size} elements but grid has {grid_size} points.") - if source.p is not None and source.p_mask is None: + if getattr(source, "p", None) is not None and getattr(source, "p_mask", None) is None: raise ValueError("source.p requires source.p_mask to be set.") - if source.p_mask is not None: + if getattr(source, "p_mask", None) is not None: mask = np.asarray(source.p_mask) is_cartesian = mask.ndim == 2 and mask.shape[0] == kgrid.dim if not is_cartesian and mask.size != grid_size: raise ValueError(f"source.p_mask has {mask.size} elements but grid has {grid_size} points.") for vel in ["ux", "uy", "uz"]: - if getattr(source, vel, None) is not None and source.u_mask is None: + if getattr(source, vel, None) is not None and getattr(source, "u_mask", None) is None: raise ValueError(f"source.{vel} requires source.u_mask to be set.") - if source.u_mask is not None: + if getattr(source, "u_mask", None) is not None: mask = np.asarray(source.u_mask) is_cartesian = mask.ndim == 2 and mask.shape[0] == kgrid.dim if not is_cartesian and mask.size != grid_size: diff --git a/notebooks/.gitkeep b/notebooks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/plans/release-strategy.md b/plans/release-strategy.md index 9b323864b..132b59bb0 100644 --- a/plans/release-strategy.md +++ b/plans/release-strategy.md @@ -141,6 +141,36 @@ warnings.warn( **Impact:** ~5700 lines → ~1200 lines (-79%) +### Memory Layout and Indexing Migration (v1.0.0) + +k-Wave is a MATLAB project. Two MATLAB conventions pervade the Python codebase: + +1. **Fortran (column-major) memory layout** — `order="F"` appears ~22 times in the solver alone. Arrays are flattened/reshaped with `order="F"` to match MATLAB's column-major storage. NumPy defaults to C (row-major). + +2. **1-based indexing** — The C++ binary expects 1-based sensor/source indices in HDF5. `matlab_find()` returns 1-based indices. `sensor.record_start_index` uses 1-based convention. + +**Current state:** Both conventions are handled at the boundaries: +- `kspace_solver.py`: Uses `order="F"` in `_expand_to_grid`, `_build_source_op`, sensor mask extraction, and field recording. The physics (FFT, PML, stepping) is order-agnostic. +- `cpp_simulation.py`: Converts to 1-based Fortran-order indices for HDF5 serialization. +- `kWaveArray.combine_sensor_data`: Uses `matlab_find` (1-based) and Fortran-order flattening. + +**Migration strategy for v1.0.0:** + +| Layer | Current | Target | Migration | +|-------|---------|--------|-----------| +| **Python solver** (`kspace_solver.py`) | F-order throughout | C-order internally, F-order at boundaries only | Audit all `order="F"` — keep only where MATLAB compat requires it (interop, HDF5). Internal field storage uses default C-order. | +| **C++ HDF5 serialization** (`cpp_simulation.py`) | F-order + 1-based | Keep as-is (binary expects this) | This is a fixed boundary — always convert at the serialization layer. | +| **MATLAB interop** (`simulate_from_dicts`, `kWavePy.py`) | F-order dicts from MATLAB | Convert at entry point | `simulate_from_dicts` converts F→C on input, C→F on output. Single conversion boundary. | +| **kWaveArray** | `matlab_find` (1-based), F-order | 0-based, C-order | Rewrite `combine_sensor_data` and `get_distributed_source_signal` to use `np.where` (0-based) and C-order. | +| **sensor.record_start_index** | 1-based (MATLAB convention) | 0-based | Accept both, deprecate 1-based in v0.7, require 0-based in v1.0. | + +**Principle:** Python-native (C-order, 0-based) everywhere internally. MATLAB compatibility at two explicit boundaries: (1) `simulate_from_dicts` for k-wave-cupy interop, (2) `cpp_simulation._write_hdf5` for the C++ binary. + +**Testing:** Add a `test_memory_layout.py` that verifies: +- `Simulation` produces identical results with C-order and F-order input arrays +- `simulate_from_dicts` correctly converts F→C→F round-trip +- `cpp_simulation` writes correct 1-based F-order HDF5 regardless of internal layout + --- ## Phase 3.5: v0.7.0 - CLI (`kwp`) diff --git a/pyproject.toml b/pyproject.toml index 7fb5ccdd8..340f86904 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ docs = [ "sphinx-mdinclude==0.6.2", "sphinx-tabs==3.4.7", "sphinx-toolbox==3.8.0", "furo==2024.8.6"] -dev = ["pre-commit==4.2.0"] +dev = ["pre-commit==4.2.0", "jupytext"] [tool.hatch.version] path = "kwave/__init__.py" diff --git a/run_examples.py b/run_examples.py index ae543ecbe..adb299c43 100644 --- a/run_examples.py +++ b/run_examples.py @@ -1,26 +1,37 @@ +"""Run all example scripts with the Python backend on CPU.""" import os import subprocess +import sys from pathlib import Path +SKIP = {"example_utils.py", "__init__.py"} -def run_python_files(directory): - # Recursively walk through the directory - for root, _, files in os.walk(directory): - for file in files: - # Check if it's a Python file - if file.endswith(".py"): - file_path = os.path.join(root, file) - print(f"Running: {file_path}") - try: - # Use subprocess to run the Python file - result = subprocess.run(["python", file_path], capture_output=True, text=True) - print(f"Output:\n{result.stdout}") - if result.stderr: - print(f"Errors:\n{result.stderr}") - except Exception as e: - print(f"Failed to run {file_path}: {e}") + +def run_examples(directory: Path): + env = {**os.environ, "KWAVE_BACKEND": "python", "KWAVE_DEVICE": "cpu", "MPLBACKEND": "Agg"} + examples = sorted(p for p in directory.rglob("*.py") if p.name not in SKIP) + failed = [] + for path in examples: + print(f"\n{'='*60}\nRunning: {path}\n{'='*60}") + result = subprocess.run([sys.executable, str(path)], env=env, capture_output=True, text=True) + if result.stdout: + print(result.stdout) + if result.returncode != 0: + print(f"FAILED (exit code {result.returncode})") + if result.stderr: + print(result.stderr[-500:]) + failed.append(str(path)) + else: + print("OK") + + print(f"\n{'='*60}") + print(f"Results: {len(examples) - len(failed)}/{len(examples)} passed") + if failed: + print("Failed:") + for f in failed: + print(f" - {f}") + sys.exit(1) if __name__ == "__main__": - directory = Path("examples/") - run_python_files(directory) + run_examples(Path("examples/")) diff --git a/tests/test_kspaceFirstOrder.py b/tests/test_kspaceFirstOrder.py index ce13f72aa..dbb25739f 100644 --- a/tests/test_kspaceFirstOrder.py +++ b/tests/test_kspaceFirstOrder.py @@ -63,6 +63,31 @@ def test_python_backend_runs(self, sim_2d): assert "p" in result assert result["p"].shape == (int(sensor.mask.sum()), int(kgrid.Nt)) + def test_env_override_backend(self, sim_2d, monkeypatch): + """KWAVE_BACKEND env var overrides the backend parameter.""" + monkeypatch.setenv("KWAVE_BACKEND", "python") + kgrid, medium, source, sensor = sim_2d + # Pass backend="cpp" but env says "python" — should run python backend (no C++ binary needed) + result = kspaceFirstOrder(kgrid, medium, source, sensor, backend="cpp") + assert "p" in result + + def test_env_override_device(self, sim_2d, monkeypatch): + """KWAVE_DEVICE env var overrides the device parameter.""" + monkeypatch.setenv("KWAVE_DEVICE", "cpu") + kgrid, medium, source, sensor = sim_2d + result = kspaceFirstOrder(kgrid, medium, source, sensor, device="cpu") + assert "p" in result + + def test_cartesian_sensor_mask_cpp_conversion(self, sim_2d): + """Cartesian sensor mask is auto-converted to binary for cpp backend.""" + kgrid, medium, source, _ = sim_2d + # Create a Cartesian sensor mask (2, N_points) — positions in meters + cart_points = np.array([[0.0, 0.5e-3, -0.5e-3], [0.0, 0.0, 0.0]]) # 3 points, 2D + sensor = kSensor(cart_points) + # save_only=True so we don't need the C++ binary + result = kspaceFirstOrder(kgrid, medium, source, sensor, backend="cpp", save_only=True, data_path=tempfile.mkdtemp()) + assert "input_file" in result + def test_cpp_save_only(self, sim_2d): kgrid, medium, source, sensor = sim_2d result = kspaceFirstOrder(kgrid, medium, source, sensor, backend="cpp", save_only=True, data_path=tempfile.mkdtemp()) diff --git a/uv.lock b/uv.lock index 535921291..158c642a1 100644 --- a/uv.lock +++ b/uv.lock @@ -52,6 +52,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/9f/fa9971d2a0c6fef64c87ba362a493a4f230eff4ea8dfb9f4c7cbdf71892e/apeye_core-1.1.5-py3-none-any.whl", hash = "sha256:dc27a93f8c9e246b3b238c5ea51edf6115ab2618ef029b9f2d9a190ec8228fbf", size = 99286 }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548 }, +] + [[package]] name = "autodocsumm" version = "0.2.14" @@ -524,6 +533,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708 }, ] +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024 }, +] + [[package]] name = "filelock" version = "3.25.2" @@ -756,6 +774,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032 }, +] + +[[package]] +name = "jupytext" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/a5/80c02f307c8ce863cb33e27daf049315e9d96979e14eead700923b5ec9cc/jupytext-1.19.1.tar.gz", hash = "sha256:82587c07e299173c70ed5e8ec7e75183edf1be289ed518bab49ad0d4e3d5f433", size = 4307829 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl", hash = "sha256:d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9", size = 170478 }, +] + [[package]] name = "k-wave-python" version = "0.5.0rc1" @@ -774,6 +849,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "jupytext" }, { name = "pre-commit" }, ] docs = [ @@ -810,6 +886,7 @@ requires-dist = [ { name = "gdown", marker = "extra == 'example'", specifier = "==5.2.0" }, { name = "h5py", specifier = "==3.15.1" }, { name = "jaxtyping", specifier = "==0.3.2" }, + { name = "jupytext", marker = "extra == 'dev'" }, { name = "matplotlib", specifier = "==3.10.3" }, { name = "numpy", specifier = ">=1.22.2,<2.3.0" }, { name = "opencv-python", specifier = "==4.11.0.86" }, @@ -965,6 +1042,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044 }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1102,6 +1191,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298 }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + [[package]] name = "mistune" version = "3.2.0" @@ -1193,6 +1303,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268 }, ] +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454 }, +] + [[package]] name = "networkx" version = "3.4.2" @@ -1616,6 +1741,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, +] + [[package]] name = "requests" version = "2.32.5" @@ -1636,6 +1775,128 @@ socks = [ { name = "pysocks" }, ] +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490 }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751 }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696 }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136 }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699 }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022 }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522 }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579 }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305 }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503 }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322 }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792 }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901 }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823 }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157 }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676 }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938 }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932 }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830 }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033 }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828 }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683 }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583 }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496 }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669 }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011 }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406 }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024 }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069 }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086 }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053 }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763 }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951 }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622 }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492 }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080 }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680 }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589 }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289 }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737 }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120 }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782 }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463 }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868 }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887 }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904 }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945 }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783 }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021 }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589 }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025 }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895 }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799 }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731 }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027 }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020 }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139 }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224 }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645 }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443 }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375 }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850 }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812 }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841 }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149 }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843 }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507 }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949 }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790 }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217 }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806 }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341 }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768 }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099 }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192 }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080 }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841 }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670 }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005 }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112 }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049 }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661 }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606 }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126 }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371 }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298 }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604 }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391 }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868 }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747 }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795 }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330 }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194 }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340 }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765 }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834 }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470 }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630 }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148 }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030 }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292 }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128 }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542 }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004 }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063 }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099 }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177 }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015 }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736 }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981 }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782 }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191 }, +] + [[package]] name = "ruamel-yaml" version = "0.19.1" @@ -2095,6 +2356,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374 }, ] +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, +] + [[package]] name = "typing-extensions" version = "4.15.0"