diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
new file mode 100644
index 000000000..ef8330854
--- /dev/null
+++ b/.github/workflows/codespell.yml
@@ -0,0 +1,25 @@
+name: Spell Check
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ spellcheck:
+ name: Spell Check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+
+ - name: Install codespell
+ run: pip install codespell
+
+ - name: Run codespell
+ run: codespell --config .codespellrc
\ No newline at end of file
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 5582fc705..1db0dd652 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -58,23 +58,22 @@ jobs:
files: |
./collectedValues
outPath: collectedValues.tar.gz
- - name: upload reference values artifact
+ - name: Upload reference values artifact
id: artifact-upload-step
- if: ${{ steps.matlab-refs-cache.outputs.cache-hit != 'true' }}
uses: actions/upload-artifact@v4
with:
name: matlab_reference_test_values
path: collectedValues.tar.gz
# overwrite: true
+
- name: Output artifact URL
- if: ${{ steps.matlab-refs-cache.outputs.cache-hit != 'true' }}
run: echo 'Artifact URL is ${{ steps.artifact-upload-step.outputs.artifact-url }}'
test:
needs: collect_references
strategy:
matrix:
os: [ "windows-latest", "ubuntu-latest" , "macos-latest"]
- python-version: [ "3.9", "3.10", "3.11", "3.12" ]
+ python-version: [ "3.10", "3.11", "3.12", "3.13" ]
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/run-examples.yml b/.github/workflows/run-examples.yml
new file mode 100644
index 000000000..9e06f0fa6
--- /dev/null
+++ b/.github/workflows/run-examples.yml
@@ -0,0 +1,56 @@
+name: Run K-Wave Examples
+
+on:
+ schedule:
+ - cron: '0 0 * * 1' # Every Monday at 00:00 UTC
+ workflow_dispatch: # Manual trigger
+
+jobs:
+ discover-examples:
+ runs-on: ubuntu-latest
+ outputs:
+ example_paths: ${{ steps.find-examples.outputs.examples }}
+ steps:
+ - 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]')
+ echo "examples=$EXAMPLES" >> "$GITHUB_OUTPUT"
+
+ run-examples:
+ needs: discover-examples
+ runs-on: ubuntu-latest
+ timeout-minutes: 60 # 1 hour timeout per example
+ strategy:
+ fail-fast: false # Continue running other examples even if one fails
+ matrix:
+ example: ${{ fromJson(needs.discover-examples.outputs.example_paths) }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Install system dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y ffmpeg
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.10' # Matches requires-python from pyproject.toml
+ cache: 'pip'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ # Install the package with example dependencies
+ pip install -e ".[example]"
+
+ - name: Run example
+ env:
+ KWAVE_FORCE_CPU: 1
+ run: |
+ echo "Running example: ${{ matrix.example }}"
+ python "${{ matrix.example }}"
\ No newline at end of file
diff --git a/.github/workflows/test_example.yml b/.github/workflows/test_example.yml
index 0d4d1d96c..06e01f807 100644
--- a/.github/workflows/test_example.yml
+++ b/.github/workflows/test_example.yml
@@ -7,7 +7,7 @@ jobs:
strategy:
matrix:
os: [ "windows-latest", "ubuntu-latest" , "macos-latest"]
- python-version: [ "3.9", "3.10", "3.11", "3.12"]
+ python-version: [ "3.10", "3.11", "3.12", "3.13" ]
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4
@@ -23,7 +23,7 @@ jobs:
run: |
python3 examples/us_bmode_linear_transducer/us_bmode_linear_transducer.py
- name: Upload example results
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: example_bmode_reconstruction_results_${{ matrix.os }}_${{ matrix.python-version }}
path: ${{ github.workspace }}/example_bmode.png
diff --git a/.github/workflows/test_optional_dependencies.yml b/.github/workflows/test_optional_dependencies.yml
index 8d7f74f0b..3179f00dc 100644
--- a/.github/workflows/test_optional_dependencies.yml
+++ b/.github/workflows/test_optional_dependencies.yml
@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [ "windows-latest", "ubuntu-latest" , "macos-latest"]
- python-version: [ "3.9", "3.10", "3.11", "3.12" ]
+ python-version: [ "3.10", "3.11", "3.12", "3.13" ]
extra_requirements: [ "test", "examples", "docs", "dev", "all" ]
runs-on: ${{matrix.os}}
steps:
@@ -26,4 +26,4 @@ jobs:
cache: 'pip'
- name: Install dependencies
run: |
- pip install '.[${{ matrix.extra_requirements }}]'
+ pip install '.[${{ matrix.extra_requirements }}]'
\ No newline at end of file
diff --git a/.github/workflows/test_pages.yml b/.github/workflows/test_pages.yml
index ef949fc79..0960c82c8 100644
--- a/.github/workflows/test_pages.yml
+++ b/.github/workflows/test_pages.yml
@@ -10,9 +10,9 @@ jobs:
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
- uses: actions/setup-python@v5
with:
- python-version: '3.9'
+ python-version: '3.10'
cache: 'pip'
- name: Build and Commit
uses: waltsims/pages@pyproject.toml-support
with:
- pyproject_toml_deps: ".[docs]"
+ pyproject_toml_deps: ".[docs]"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 18a37dc47..2b6cf03ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,8 @@ tests/reference_outputs
kspaceFirstOrder-*_metadata.json
+*.nii
+*.vtk
+*.vtk
+examples/benchmarks/8/runner.log
+*.vtk
diff --git a/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.py b/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.py
index a5a4fc645..ee43575a3 100644
--- a/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.py
+++ b/examples/at_focused_annular_array_3D/at_focused_annular_array_3D.py
@@ -161,7 +161,7 @@
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]
+x_vec = kgrid.x_vec[source_x_offset + 1: -1, :] - kgrid.x_vec[source_x_offset]
y_vec = kgrid.y_vec
# =========================================================================
diff --git a/examples/benchmarks/8/ph1-bm8-freefield-sc2.py b/examples/benchmarks/8/ph1-bm8-freefield-sc2.py
new file mode 100644
index 000000000..7d538fedf
--- /dev/null
+++ b/examples/benchmarks/8/ph1-bm8-freefield-sc2.py
@@ -0,0 +1,1109 @@
+import numpy as np
+
+import logging
+import sys
+import matplotlib.pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+from cycler import cycler
+
+from copy import deepcopy
+
+import h5py
+
+from skimage import measure
+from skimage.segmentation import find_boundaries
+from scipy.interpolate import interpn
+from scipy.interpolate import RegularGridInterpolator
+
+from kwave.data import Vector
+from kwave.utils.kwave_array import kWaveArray
+from kwave.utils.checks import check_stability
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.ksensor import kSensor
+from kwave.utils.signals import create_cw_signals
+from kwave.utils.filters import extract_amp_phase
+from kwave.kspaceFirstOrder3D import kspaceFirstOrder3DG
+
+from kwave.options.simulation_options import SimulationOptions
+from kwave.options.simulation_execution_options import SimulationExecutionOptions
+
+import pyvista as pv
+
+
+# create logger
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+# create console and file handlers and set level to debug
+ch = logging.StreamHandler(sys.stdout)
+ch.setLevel(logging.DEBUG)
+fh = logging.FileHandler(filename='runner.log')
+fh.setLevel(logging.DEBUG)
+
+# create formatter
+formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s')
+# add formatter to ch, fh
+ch.setFormatter(formatter)
+fh.setFormatter(formatter)
+
+# add ch, fh to logger
+logger.addHandler(ch)
+logger.addHandler(fh)
+
+# propagate
+ch.propagate = True
+fh.propagate = True
+logger.propagate = True
+
+verbose: bool = True
+savePlotting: bool = True
+useMaxTimeStep: bool = True
+
+tag = 'bm8'
+res = '1mm'
+transducer = 'sc2'
+
+mask_folder = 'C:/Users/dsinden/GitHub/k-wave-python/data/'
+
+mask_filename = mask_folder + 'skull_mask_' + tag + '_dx_' + res + '.mat'
+
+if verbose:
+ logger.info(mask_filename)
+
+data = h5py.File(mask_filename, 'r')
+
+if verbose:
+ logger.info( list(data.keys()) )
+
+# is given in millimetres
+dx = data['dx'][:].item()
+
+# scale to metres
+dx = dx / 1000.0
+dy = dx
+dz = dx
+
+xi = np.squeeze(np.asarray(data['xi'][:]))
+yi = np.squeeze(np.asarray(data['yi'][:]))
+zi = np.squeeze(np.asarray(data['zi'][:]))
+
+matlab_shape = np.shape(xi)[0], np.shape(yi)[0], np.shape(zi)[0]
+
+skull_mask = np.squeeze(data['skull_mask'][:]).astype(bool)
+brain_mask = np.squeeze(data['brain_mask'][:]).astype(bool)
+
+# convert to Fortran-ordered arrays
+skull_mask = np.reshape(skull_mask.flatten(), matlab_shape, order='F')
+brain_mask = np.reshape(brain_mask.flatten(), matlab_shape, order='F')
+
+# create water mask
+water_mask = np.ones(skull_mask.shape, dtype=int) - (skull_mask.astype(int) +
+ brain_mask.astype(int))
+water_mask = water_mask.astype(bool)
+
+# orientation of axes
+skull_mask = np.swapaxes(skull_mask, 0, 2)
+brain_mask = np.swapaxes(brain_mask, 0, 2)
+water_mask = np.swapaxes(water_mask, 0, 2)
+
+# # cropping settings - was 10
+skull_mask = skull_mask[:, :, 16:]
+brain_mask = brain_mask[:, :, 16:]
+water_mask = water_mask[:, :, 16:]
+
+# set domains sizes
+Nx, Ny, Nz = skull_mask.shape
+
+msg = "new shape=" + str(skull_mask.shape)
+if verbose:
+ logger.info(msg)
+
+if (transducer == 'sc1'):
+ # curved element with focal depth of 64 mm, so is scaled by resolution to give value in grid point
+ # bowl radius of curvature [m]
+ msg = "transducer is focused"
+ focus = int(64 / data['dx'][:].item())
+ focus_coords = [(Nx - 1) // 2, (Ny - 1) // 2, focus]
+ bowl_coords = [(Nx - 1) // 2, (Ny - 1) // 2, 0]
+
+if (transducer == 'sc2'):
+ # planar element
+ msg = "transducer is planar"
+ focus_coords = [(Nx - 1) // 2, (Ny - 1) // 2, (Nz - 1) // 2]
+ disc_coords = [(Nx - 1) // 2, (Ny - 1) // 2, 0]
+
+if verbose:
+ logger.info(msg)
+
+# =========================================================================
+# DEFINE THE MATERIAL PROPERTIES
+# =========================================================================
+
+# water
+sound_speed = 1500.0 * np.ones(skull_mask.shape)
+density = 1000.0 * np.ones(skull_mask.shape)
+alpha_coeff = np.zeros(skull_mask.shape)
+
+# non-dispersive
+alpha_power = 2.0
+
+# skull
+sound_speed[skull_mask] = 2800.0
+density[skull_mask] = 1850.0
+alpha_coeff[skull_mask] = 4.0
+
+# brain
+sound_speed[brain_mask] = 1560.0
+density[brain_mask] = 1040.0
+alpha_coeff[brain_mask] = 0.3
+
+c0_min = np.min(sound_speed.flatten())
+c0_max = np.min(sound_speed.flatten())
+
+medium = kWaveMedium(
+ sound_speed=sound_speed,
+ density=density,
+ alpha_coeff=alpha_coeff,
+ alpha_power=alpha_power
+)
+
+# =========================================================================
+# DEFINE THE TRANSDUCER SETUP
+# =========================================================================
+
+# single spherical transducer
+if (transducer == 'sc1'):
+
+ # bowl radius of curvature [m]
+ source_roc = 64.0e-3
+
+ # as we will use the bowl element this has to be a int or float
+ diameters = 64.0e-3
+
+elif (transducer == 'sc2'):
+
+ # diameter of the disc
+ diameter = 10e-3
+
+# frequency [Hz]
+freq = 500e3
+
+# source pressure [Pa]
+source_amp = np.array([60e3])
+
+# phase [rad]
+source_phase = np.array([0.0])
+
+
+# =========================================================================
+# DEFINE COMPUTATIONAL PARAMETERS
+# =========================================================================
+
+# wavelength
+k_min = c0_min / freq
+
+# points per wavelength
+ppw = k_min / dx
+
+# number of periods to record
+record_periods: int = 3
+
+# compute points per period
+ppp: int = 20
+
+# CFL number determines time step
+cfl = (ppw / ppp)
+
+
+# =========================================================================
+# DEFINE THE KGRID
+# =========================================================================
+
+grid_size_points = Vector([Nx, Ny, Nz])
+
+grid_spacing_meters = Vector([dx, dy, dz])
+
+# create the k-space grid
+kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)
+
+
+# =========================================================================
+# DEFINE THE TIME VECTOR
+# =========================================================================
+
+# compute corresponding time stepping
+dt = 1.0 / (ppp * freq)
+
+# compute corresponding time stepping
+dt = (c0_min / c0_max) / (float(ppp) * freq)
+
+dt_stability_limit = check_stability(kgrid, medium)
+msg = "dt_stability_limit=" + str(dt_stability_limit) + ", dt=" + str(dt)
+if verbose:
+ logger.info(msg)
+
+if (useMaxTimeStep and (not np.isfinite(dt_stability_limit)) and
+ (dt_stability_limit < dt)):
+ dt_old = dt
+ ppp = np.ceil( 1.0 / (dt_stability_limit * freq) )
+ dt = 1.0 / (ppp * freq)
+ if verbose:
+ logger.info("updated dt")
+else:
+ if verbose:
+ logger.info("not updated dt")
+
+
+# calculate the number of time steps to reach steady state
+t_end = np.sqrt(kgrid.x_size**2 + kgrid.y_size**2) / c0_min
+
+# create the time array using an integer number of points per period
+Nt = round(t_end / dt)
+
+# make time array
+kgrid.setTime(Nt, dt)
+
+# calculate the actual CFL after adjusting for dt
+cfl_actual = 1.0 / (dt * freq)
+
+if verbose:
+ logger.info('PPW = ' + str(ppw))
+ logger.info('CFL = ' + str(cfl_actual))
+ logger.info('PPP = ' + str(ppp))
+
+
+# =========================================================================
+# DEFINE THE SOURCE PARAMETERS
+# =========================================================================
+
+if verbose:
+ logger.info("kSource")
+
+# create empty kWaveArray this specfies the transducer properties
+karray = kWaveArray(bli_tolerance=0.01,
+ upsampling_rate=16,
+ single_precision=True)
+
+if (transducer == 'sc1'):
+
+ # set bowl position and orientation
+ bowl_pos = [kgrid.x_vec[bowl_coords[0]].item(),
+ kgrid.y_vec[bowl_coords[1]].item(),
+ kgrid.z_vec[bowl_coords[2]].item()]
+
+ focus_pos = [kgrid.x_vec[focus_coords[0]].item(),
+ kgrid.y_vec[focus_coords[1]].item(),
+ kgrid.z_vec[focus_coords[2]].item()]
+
+ # add bowl shaped element
+ karray.add_bowl_element(bowl_pos, source_roc, diameters, focus_pos)
+
+elif (transducer == 'sc2'):
+
+ # set disc position
+ position = [kgrid.x_vec[disc_coords[0]].item(),
+ kgrid.y_vec[disc_coords[1]].item(),
+ kgrid.z_vec[disc_coords[2]].item()]
+
+ # arbitrary position
+ focus_pos = [kgrid.x_vec[focus_coords[0]].item(),
+ kgrid.y_vec[focus_coords[1]].item(),
+ kgrid.z_vec[focus_coords[2]].item()]
+
+ # add disc-shaped planar element
+ karray.add_disc_element(position, diameter, focus_pos)
+
+# create time varying source
+source_sig = create_cw_signals(np.squeeze(kgrid.t_array),
+ freq,
+ source_amp,
+ source_phase)
+
+# 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)
+
+
+# =========================================================================
+# DEFINE THE SENSOR PARAMETERS
+# =========================================================================
+
+if verbose:
+ logger.info("kSensor")
+
+sensor = kSensor()
+
+# set sensor mask: the mask says at which points data should be recorded
+sensor.mask = np.ones((Nx, Ny, Nz), dtype=bool)
+
+# set the record type: record the pressure waveform
+sensor.record = ['p']
+
+# record 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
+# =========================================================================
+
+DATA_CAST = 'single'
+DATA_PATH = './'
+
+input_filename = tag + '_' + transducer + '_' + res + '_input.h5'
+output_filename = tag + '_' + transducer + '_' + res + '_output.h5'
+
+# set input options
+if verbose:
+ logger.info("simulation_options")
+
+# options for writing to file, but not doing simulations
+simulation_options = SimulationOptions(
+ data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename,
+ output_filename=output_filename,
+ save_to_disk_exit=False,
+ data_path=DATA_PATH,
+ pml_inside=False)
+
+if verbose:
+ logger.info("execution_options")
+
+execution_options = SimulationExecutionOptions(
+ is_gpu_simulation=True,
+ delete_data=False,
+ verbose_level=2)
+
+
+
+# =========================================================================
+# RUN THE SIMULATION
+# =========================================================================
+
+if verbose:
+ logger.info("kspaceFirstOrder3DG")
+
+sensor_data = kspaceFirstOrder3DG(
+ medium=medium,
+ kgrid=kgrid,
+ source=source,
+ sensor=sensor,
+ simulation_options=simulation_options,
+ execution_options=execution_options)
+
+
+# =========================================================================
+# POST-PROCESS
+# =========================================================================
+
+
+# * needs p
+
+if verbose:
+ logger.info("post processing")
+
+# sampling frequency
+fs = 1.0 / kgrid.dt
+
+if verbose:
+ logger.info("extract_amp_phase")
+
+# get Fourier coefficients
+amp, _, _ = extract_amp_phase(sensor_data['p'].T, fs, freq, dim=1,
+ fft_padding=1, window='Rectangular')
+
+# reshape data: matlab uses Fortran ordering
+p = np.reshape(amp, (Nx, Ny, Nz), order='F')
+
+x = np.linspace(-Nx // 2, Nx // 2 - 1, Nx)
+y = np.linspace(-Ny // 2, Ny // 2 - 1, Ny)
+z = np.linspace(-Nz // 2, Nz // 2 - 1, Nz)
+x, y, z = np.meshgrid(x, y, z, indexing='ij')
+
+pmax = np.nanmax(p)
+max_loc = np.unravel_index(np.nanargmax(p), p.shape, order='C')
+
+p_water = np.empty_like(p)
+p_water.fill(np.nan)
+p_water[water_mask] = p[water_mask]
+pmax_water = np.nanmax(p_water)
+max_loc_water = np.unravel_index(np.nanargmax(p_water), p.shape, order='C')
+
+p_skull = np.empty_like(p)
+p_skull.fill(np.nan)
+p_skull[skull_mask] = p[skull_mask]
+pmax_skull = np.nanmax(p_skull)
+max_loc_skull = np.unravel_index(np.nanargmax(p_skull), p.shape, order='C')
+
+p_brain = np.empty_like(p)
+p_brain.fill(np.nan)
+p_brain[brain_mask] = p[brain_mask]
+pmax_brain = np.nanmax(p_brain)
+max_loc_brain = np.unravel_index(np.nanargmax(p_brain), p.shape, order='C')
+
+# domain axes
+x_vec = np.linspace(kgrid.x_vec[0].item(), kgrid.x_vec[-1].item(), kgrid.Nx)
+y_vec = np.linspace(kgrid.y_vec[0].item(), kgrid.y_vec[-1].item(), kgrid.Ny)
+z_vec = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+
+# colours
+cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
+
+# brain axes
+# x
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[0]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+x_x = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_brain[2]].item()]
+y_x = [kgrid.x_vec[indy].item(), kgrid.x_vec[max_loc_brain[0]].item()]
+coefficients_x = np.polyfit(x_x, y_x, 1)
+polynomial_x = np.poly1d(coefficients_x)
+axis = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+beam_axis_x = polynomial_x(z_vec)
+# y
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[1]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+x_y = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_brain[2]].item()]
+y_y = [kgrid.y_vec[indy].item(), kgrid.y_vec[max_loc_brain[1]].item()]
+coefficients_y = np.polyfit(x_y, y_y, 1)
+polynomial_y = np.poly1d(coefficients_y)
+beam_axis_y = polynomial_y(z_vec)
+# beam axis
+beam_axis = np.vstack((beam_axis_x, beam_axis_y, z_vec)).T
+# interpolate for pressure on brain axis
+beam_pressure_brain = interpn((x_vec, y_vec, z_vec) , p, beam_axis,
+ method='linear', bounds_error=False, fill_value=np.nan)
+
+# skull axes
+# x
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[0]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+x_x = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_skull[2]].item()]
+y_x = [kgrid.x_vec[indy].item(), kgrid.x_vec[max_loc_skull[0]].item()]
+coefficients_x = np.polyfit(x_x, y_x, 1)
+polynomial_x = np.poly1d(coefficients_x)
+axis = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+beam_axis_x = polynomial_x(z_vec)
+# y
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[1]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+x_y = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_skull[2]].item()]
+y_y = [kgrid.y_vec[indy].item(), kgrid.y_vec[max_loc_skull[1]].item()]
+coefficients_y = np.polyfit(x_y, y_y, 1)
+polynomial_y = np.poly1d(coefficients_y)
+beam_axis_y = polynomial_y(z_vec)
+# beam axis
+beam_axis = np.vstack((beam_axis_x, beam_axis_y, z_vec)).T
+# interpolate for pressure
+beam_pressure_skull = interpn((x_vec, y_vec, z_vec) , p, beam_axis,
+ method='linear', bounds_error=False, fill_value=np.nan)
+
+
+
+# plot pressure on through centre lines
+fig1, ax1 = plt.subplots()
+# ax1.plot(p[(Nx-1)//2, (Nx-1)//2, :] / 1e6, label='geometric')
+ax1.plot(beam_pressure_brain / np.max(beam_pressure_brain))
+ax1.plot(p[focus_coords[0], focus_coords[1], :] / np.max(p))
+# ax1.plot(beam_pressure_skull / 1e6, label='skull')
+# ax1.hlines(pmax_brain / np.max(beam_pressure_brain), 0, len(z_vec), color=cycle[1], linestyle='dashed', lw=0.5)
+# ax1.hlines(pmax_skull / 1e6, 0, len(z_vec), color=cycle[2], linestyle='dashed', lw=0.5)
+ax1.set(xlabel='Axial Position [mm]',
+ ylabel='Pressure []',
+ title='Centreline Pressure')
+ax1.legend()
+ax1.grid(True)
+
+
+
+def get_edges(mask, fill_with_nan=True):
+ """returns the mask as a float array and Np.NaN"""
+ edges = find_boundaries(mask, mode='thin').astype(np.float32)
+ if fill_with_nan:
+ edges[edges == 0] = np.nan
+ return edges
+
+# contouring block
+
+edges_x = get_edges(np.transpose(skull_mask[max_loc_brain[0], :, :]).astype(int), fill_with_nan=False)
+edges_y = get_edges(np.transpose(skull_mask[:, max_loc_brain[1], :]).astype(int), fill_with_nan=False)
+edges_z = get_edges(np.transpose(skull_mask[:, :, max_loc_brain[2]]).astype(int), fill_with_nan=False)
+
+contour_x, num_x = measure.label(edges_x, background=0, return_num=True, connectivity=2)
+contour_y, num_y = measure.label(edges_y, background=0, return_num=True, connectivity=2)
+contour_z, num_z = measure.label(edges_z, background=0, return_num=True, connectivity=2)
+
+if verbose:
+ msg = "size of contours:" + str(np.shape(contour_x)) + ", " + str(np.shape(contour_y)) + ", " + str(np.shape(contour_z)) + "."
+ logger.info(msg)
+ msg = "number of contours: (" + str(num_x) + ", " + str(num_y) + ", " + str(num_z) + ")."
+ logger.info(msg)
+
+jmax = 0
+jmin = Ny
+i_inner = None
+i_outer = None
+# for a number of contours
+for i in range(num_x):
+ idx = int(np.shape(contour_x)[1] // 2)
+ j = np.argmax(np.where(contour_x[:, idx]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i + 1
+ k = np.argmin(np.where(contour_x[:, idx]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i + 1
+contours_x_inner = measure.find_contours(np.where(contour_x==i_inner, 1, 0))
+if not contours_x_inner:
+ logger.warning("size of contours_x_inner is zero")
+contours_x_outer = measure.find_contours(np.where(contour_x==i_outer, 1, 0))
+if not contours_x_outer:
+ logger.warning("size of contours_x_outer is zero")
+inner_index_x = float(Ny)
+outer_index_x = float(0)
+for i in range(len(contours_x_inner)):
+ x_min = np.min(contours_x_inner[i][:, 1])
+ if (x_min < inner_index_x):
+ inner_index_x = i
+for i in range( len(contours_x_outer) ):
+ x_max = np.max(contours_x_outer[i][:, 1])
+ if (x_max > outer_index_x):
+ outer_index_x = i
+
+jmax = 0
+jmin = Nx
+i_inner = None
+i_outer = None
+for i in range(num_y):
+ idy: int = int(np.shape(contour_y)[1] // 2)
+ j = np.argmax(np.where(contour_y[:, idy]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i + 1
+ k = np.argmin(np.where(contour_y[:, idy]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i + 1
+contours_y_inner = measure.find_contours(np.where(contour_y==i_inner, 1, 0))
+if not contours_y_inner:
+ logger.warning("size of contours_y_inner is zero")
+contours_y_outer = measure.find_contours(np.where(contour_y==i_outer, 1, 0))
+if not contours_y_outer:
+ logger.warning("size of contours_y_outer is zero")
+inner_index_y = float(Nx)
+outer_index_y = float(0)
+for i in range( len(contours_y_inner) ):
+ y_min = np.min(contours_y_inner[i][:, 1])
+ if (y_min < inner_index_y):
+ inner_index_y = i
+for i in range( len(contours_y_outer) ):
+ y_max = np.max(contours_y_outer[i][:, 1])
+ if (y_max > outer_index_y):
+ outer_index_y = i
+
+jmax = 0
+jmin = Ny
+i_inner = None
+i_outer = None
+for i in range(num_z):
+ idz: int = int(np.shape(contour_z)[1] // 2)
+ j = np.argmax(np.where(contour_z[:, idz]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i+1
+ k = np.argmin(np.where(contour_z[:, idz]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i+1
+
+contours_z_inner = measure.find_contours(np.where(contour_z==i_inner, 1, 0))
+if not contours_z_inner:
+ logger.warning("size of contours_z_inner is zero")
+else:
+ inner_index_z = float(Nx)
+ for i in range( len(contours_z_inner) ):
+ z_min = np.min(contours_z_inner[i][:, 1])
+ if (z_min < inner_index_z):
+ inner_index_z = i
+
+contours_z_outer = measure.find_contours(np.where(contour_z==i_outer, 1, 0))
+if not contours_z_outer:
+ logger.warning("size of contours_z_outer is zero")
+else:
+ outer_index_z = float(0)
+ for i in range( len(contours_z_outer) ):
+ z_max = np.max(contours_z_outer[i][:, 1])
+ if (z_max > outer_index_z):
+ outer_index_z = i
+
+# end of contouring block
+
+edges_x = get_edges(np.transpose(skull_mask[max_loc_brain[0], :, :]).astype(int))
+edges_y = get_edges(np.transpose(skull_mask[:, max_loc_brain[1], :]).astype(int))
+edges_z = get_edges(np.transpose(skull_mask[:, :, max_loc_brain[2]]).astype(int), fill_with_nan=True)
+
+# plot the pressure field at mid point along z axis
+fig2, ax2 = plt.subplots()
+im2 = ax2.imshow(p[:, :, max_loc_brain[2]] / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='lower',
+ cmap='viridis')
+
+if not contours_z_inner:
+ ax2.imshow(edges_z, aspect='auto', interpolation='none',
+ cmap='Greys', origin='upper')
+else:
+ ax2.plot(contours_z_inner[inner_index_z][:, 1],
+ contours_z_inner[inner_index_z][:, 0], 'w', linewidth=0.5)
+if not contours_z_outer:
+ pass
+else:
+ ax2.plot(contours_z_outer[outer_index_z][:, 1],
+ contours_z_outer[outer_index_z][:, 0], 'w', linewidth=0.5)
+
+ax2.set(xlabel=r'$x$ [mm]',
+ ylabel=r'$y$ [mm]',
+ title='Pressure Field')
+ax2.grid(False)
+divider2 = make_axes_locatable(ax2)
+cax2 = divider2.append_axes("right", size="5%", pad=0.05)
+cbar_2 = fig2.colorbar(im2, cax=cax2)
+cbar_2.ax.set_title('[MPa]', fontsize='small')
+
+pwater_max_x = np.nanmax(p_water[max_loc_brain[0], :, :].flatten())
+pskull_max_x = np.nanmax(p_skull[max_loc_brain[0], :, :].flatten())
+pbrain_max_x = np.nanmax(p_brain[max_loc_brain[0], :, :].flatten())
+
+pwater_max_y = np.nanmax(p_water[:, max_loc_brain[1], :].flatten())
+pskull_max_y = np.nanmax(p_skull[:, max_loc_brain[1], :].flatten())
+pbrain_max_y = np.nanmax(p_brain[:, max_loc_brain[1], :].flatten())
+
+fig3, (ax3a, ax3b) = plt.subplots(1,2)
+im3a_water = ax3a.imshow(p_water[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='cool')
+im3a_skull = ax3a.imshow(p_skull[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='turbo')
+im3a_brain = ax3a.imshow(p_brain[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax3a.plot(contours_x_inner[inner_index_x][:, 1],
+ contours_x_inner[inner_index_x][:, 0], 'k', linewidth=0.5)
+ax3a.plot(contours_x_outer[outer_index_x][:, 1],
+ contours_x_outer[outer_index_x][:, 0], 'k', linewidth=0.5)
+
+ax3a.grid(False)
+ax3a.axes.get_yaxis().set_visible(False)
+ax3a.axes.get_xaxis().set_visible(False)
+divider3a = make_axes_locatable(ax3a)
+cax3a = divider3a.append_axes("right", size="5%", pad=0.05)
+cbar_3a = fig3.colorbar(im3a_brain, cax=cax3a)
+cbar_3a.ax.set_title('[kPa]', fontsize='small')
+ax3b.imshow(p_water[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='cool')
+ax3b.imshow(p_skull[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='turbo')
+im3b_brain = ax3b.imshow(p_brain[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax3b.grid(False)
+ax3b.axes.get_yaxis().set_visible(False)
+ax3b.axes.get_xaxis().set_visible(False)
+divider3b = make_axes_locatable(ax3b)
+cax3b = divider3b.append_axes("right", size="5%", pad=0.05)
+cbar_3b = fig3.colorbar(im3b_brain, cax=cax3b)
+cbar_3b.ax.set_title('[Pa]', fontdict={'fontsize':8})
+
+
+fig4, ax4 = plt.subplots()
+if not contours_z_inner:
+ pass
+else:
+ ax4.plot(contours_z_inner[inner_index_z][:, 1],
+ contours_z_inner[inner_index_z][:, 0], 'w', linewidth=0.5)
+if not contours_z_outer:
+ pass
+else:
+ ax4.plot(contours_z_outer[outer_index_z][:, 1],
+ contours_z_outer[outer_index_z][:, 0], 'w', linewidth=0.5)
+
+
+fig5, (ax5a, ax5b) = plt.subplots(1,2)
+im5a = ax5a.imshow(p[max_loc_brain[0], :, :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+im5a_boundary = ax5a.imshow(edges_x, aspect='auto', interpolation='none',
+ cmap='Greys', origin='upper', alpha=0.75)
+ax5a.grid(False)
+ax5a.axes.get_yaxis().set_visible(False)
+ax5a.axes.get_xaxis().set_visible(False)
+divider5a = make_axes_locatable(ax5a)
+cax5a = divider5a.append_axes("right", size="5%", pad=0.05)
+cbar_5a = fig5.colorbar(im5a, cax=cax5a)
+cbar_5a.ax.set_title('[MPa]', fontsize='small')
+im5b = ax5b.imshow(p[:, max_loc_brain[1], :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+im5b_boundary = ax5b.imshow(edges_y, aspect='auto', interpolation='none',
+ cmap='Greys',origin='upper', alpha=0.75)
+ax5b.grid(False)
+ax5b.axes.get_yaxis().set_visible(False)
+ax5b.axes.get_xaxis().set_visible(False)
+divider5b = make_axes_locatable(ax5b)
+cax5b = divider5b.append_axes("right", size="5%", pad=0.05)
+cbar_5b = fig5.colorbar(im5b, cax=cax5b)
+cbar_5b.ax.set_title('[MPa]', fontsize='small')
+
+all_contours_x = []
+for i in range(num_x):
+ all_contours_x.append(measure.find_contours(np.where(contour_x==(i+1), 1, 0)))
+
+all_contours_y = []
+for i in range(num_y):
+ all_contours_y.append(measure.find_contours(np.where(contour_y==(i+1), 1, 0)))
+
+custom_cycler = cycler(ls=['-', '--', ':', '-.'])
+
+fig6, (ax6a, ax6b) = plt.subplots(1,2)
+
+ax6a.set_prop_cycle(custom_cycler)
+im6a = ax6a.imshow(p[max_loc_brain[0], :, :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+for idx, contour in enumerate(all_contours_x):
+ for i in range( len(contour) ):
+ if ((idx == 0) and (i == 1)) or ((idx == 1) and (i == 0)):
+ ax6a.plot(contour[i][:, 1], contour[i][:, 0], ls='-', c='w',
+ linewidth=0.5)
+ax6a.grid(False)
+ax6a.axes.get_yaxis().set_visible(False)
+ax6a.axes.get_xaxis().set_visible(False)
+divider6a = make_axes_locatable(ax5a)
+cax6a = divider6a.append_axes("right", size="5%", pad=0.05)
+cbar_6a = fig6.colorbar(im6a, cax=cax6a)
+cbar_6a.ax.set_title('[MPa]', fontsize='small')
+im6b = ax6b.imshow(p[:, max_loc_brain[1], :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax6b.set_prop_cycle(custom_cycler)
+for idx, contour in enumerate(all_contours_y):
+ for i in range( len(contour) ):
+ if (idx == 0) and (i==1):
+ ax6b.plot(contour[i][:, 1], contour[i][:, 0], ls='-', c='w',
+ linewidth=0.5)
+# ax6b.legend()
+ax6b.grid(False)
+ax6b.axes.get_yaxis().set_visible(False)
+ax6b.axes.get_xaxis().set_visible(False)
+divider6b = make_axes_locatable(ax6b)
+cax6b = divider6b.append_axes("right", size="5%", pad=0.05)
+cbar_6b = fig6.colorbar(im6b, cax=cax6b)
+cbar_6b.ax.set_title('[MPa]', fontsize='small')
+
+# plt.show()
+
+plotter = pv.Plotter()
+
+pmax = np.nanmax(p)
+pmin = np.nanmin(p)
+
+grid = pv.ImageData()
+grid.dimensions = np.array(p.shape) + 1
+grid.spacing = (1, 1, 1)
+grid.cell_data['pressure'] = np.ravel(p, order="F")
+
+xslice_depth = max_loc_brain[0]
+yslice_depth = max_loc_brain[1]
+zslice_depth = max_loc_brain[2]
+
+
+
+slice_x_focus = grid.slice(normal='x', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+slice_y_focus = grid.slice(normal='y', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+slice_z_focus = grid.slice(normal='z', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+
+slice_z_tx = grid.slice(normal='-z', origin=disc_coords,
+ generate_triangles=False, contour=False, progress_bar=False)
+
+slice_z_rx = grid.slice(normal='z', origin=[(Nx-1) // 2, (Ny - 1) // 2, Nz-1],
+ generate_triangles=False, contour=False, progress_bar=False)
+
+slice_array = slice_z_rx.cell_data['pressure'].reshape(grid.dimensions[0]-1, grid.dimensions[1]-1)
+
+# now get points on skull surfaces
+verts, faces, normals, _ = measure.marching_cubes(skull_mask, 0)
+
+vfaces = np.column_stack((np.ones(len(faces),) * 3, faces)).astype(int)
+
+x = np.arange(p.shape[0]) # X-coordinates
+y = np.arange(p.shape[1]) # Y-coordinates
+z = np.arange(p.shape[2]) # Z-coordinates
+
+# set up a interpolator
+interpolator = RegularGridInterpolator((x, y, z), p)
+
+# get the pressure values on the vertices
+interpolated_values = interpolator(verts)
+
+# set up mesh for skull surface
+mesh = pv.PolyData(verts, vfaces)
+mesh['Normals'] = normals
+
+# Assign interpolated data to mesh
+mesh.point_data['abs pressure'] = interpolated_values
+
+# clip data
+mesh.point_data['abs pressure'] = np.where(mesh.point_data['abs pressure'] > pmax_brain, pmax_brain, mesh.point_data['abs pressure'] )
+
+if verbose:
+ msg = 'focus in brain: ' + str(max_loc_brain) + ', mid point: ' + str(disc_coords) + ' last plane: ' + str(np.unravel_index(np.argmax(slice_array), slice_array.shape))
+ logger.info(msg)
+
+# Choose a colormap
+plotter.add_mesh(mesh, scalars='abs pressure', opacity=0.25, show_edges=False, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=True)
+plotter.add_mesh(slice_x_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_y_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_tx, opacity=0.75, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_rx, opacity=0.75, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.show_axes()
+plotter.show_bounds()
+
+# plotter.show()
+
+plotter1 = pv.Plotter()
+plotter1.add_mesh(mesh, scalars='abs pressure',
+ opacity=0.25, show_edges=False, cmap='viridis',
+ clim=[pmin, pmax_brain], show_scalar_bar=True, above_color='yellow')
+
+
+# copy mesh
+back_mesh = deepcopy(mesh)
+front_mesh = deepcopy(mesh)
+
+max_z = mesh.points[:, 2].max()
+half_max_z = max_z / 2.0
+
+# Filter points with z value greater than half the maximum distance.
+# First get all values which should be set to zero.
+# This means the value near the trandsucer or normals pointing in the positive z direction, so the outer surface of the skull
+filtered_indices_front = np.squeeze(np.where((mesh.points[:, 2] > half_max_z) and (mesh.point_data['Normals'][:, 2] > 0.0)))
+
+print(np.sum(np.where((mesh.points[:, 2] < half_max_z))))
+
+print(np.sum(np.where((mesh.point_data['Normals'][:, 2] > 0.0))))
+
+print(np.sum(filtered_indices_front))
+
+print(np.sum(np.logical_not(filtered_indices_front)))
+
+# Set the scalar values of the front of the skull points to zero
+filtered_scalar_values_front = front_mesh.point_data['abs pressure'][filtered_indices_front]
+front_mesh.point_data['abs pressure'][np.logical_not(filtered_indices_front)] = 0.0
+# dataset complete.
+
+# Filter points with z value less than half the maximum distance
+filtered_indices = np.where(np.logical_not(filtered_indices_front))
+# set all values in front to zero
+back_mesh.point_data['abs pressure'][filtered_indices_front] = 0.0
+
+# filtered_scalar_values = back_mesh.point_data['abs pressure'][filtered_indices]
+
+# Find the index of the maximum value in the filtered scalar values
+max_index = np.argmax(back_mesh.point_data['abs pressure'])
+max_value = back_mesh.point_data['abs pressure'][max_index]
+
+# Find the location of the maximum value in the filtered scalar values
+filtered_scalar_values = front_mesh.point_data['abs pressure'][filtered_indices]
+
+max_index_front = np.argmax(filtered_scalar_values)
+max_location_index = filtered_indices[max_index_front]
+max_location = back_mesh.points[max_location_index]
+
+
+
+# filtered_indices_rev = np.where(outer_mesh.points[:, 2] > half_max_z)[0]
+# filtered_scalar_values_rev = outer_mesh.point_data['abs pressure'][filtered_indices_rev]
+# front_mesh.point_data['abs pressure'][filtered_indices_rev] = 0.0
+
+# max_index_front = np.argmax(filtered_scalar_values_front)
+# max_location_index_front = filtered_indices_rev[max_index_front]
+# max_location_front = front_mesh.points[max_location_index_front]
+# max_value_front = filtered_scalar_values[max_index_front]
+
+# output to screen
+print("\tIndex of maximum scalar value:", max_index)
+# print("\tIndex of location of maximum scalar value:", max_location_index)
+print("\tLocation of maximum scalar value:", max_location)
+print("\tMaximum scalar value:", max_value)
+print("\tMinimum scalar value:", pmin)
+
+half_max_value = max_value / 2.0
+# get the contour of the half max value in filtered region
+contour = back_mesh.contour([half_max_value])
+print(contour)
+
+# Define a scalar range of the region to extract from filtered region
+peak_range = [half_max_value, max_value]
+# Extract the mesh of the region around the max_value within the range
+peak_mesh = back_mesh.connectivity(extraction_mode='point_seed', variable_input=max_index, scalar_range=peak_range)
+
+# Extract the mesh of the region around the max_value within the range
+# peak_mesh = back_mesh.connectivity(extraction_mode='closest', variable_input=max_location_index, scalar_range=peak_range)
+
+# plotter1.show()
+
+#------------
+# get the mesh on the skull.
+# * clip orientation: x, y or z axis
+# * clip direction: less than / greater than
+# * clip position: half way.
+# * clip value: 0.0
+# * contour value: half of the maximum value in filtered region
+all_regions = mesh.connectivity('all')
+region_ids = np.unique(all_regions['RegionId'])
+
+print("Number of regions:", len(region_ids), region_ids)
+
+# outer_mesh = mesh.connectivity('largest')
+# # inner_mesh = mesh.connectivity('specified', region_ids=region_ids[1])
+
+# max_z = outer_mesh.points[:, 2].max()
+# half_max_z = max_z / 2.0
+
+# # mesh has points and point_data. keep all points, but set the data to zero before half_max_z
+
+# filtered_indices_rev = np.where(outer_mesh.points[:, 2] > half_max_z)[0]
+# filtered_scalar_values_rev = outer_mesh.point_data['abs pressure'][filtered_indices_rev]
+# front_mesh.point_data['abs pressure'][filtered_indices_rev] = 0.0
+
+# max_index_front = np.argmax(filtered_scalar_values_front)
+# max_location_index_front = filtered_indices_rev[max_index_front]
+# max_location_front = front_mesh.points[max_location_index_front]
+# max_value_front = filtered_scalar_values[max_index_front]
+
+
+# print("\tIndex of maximum scalar value:", max_index_front)
+# print("\tIndex of location of maximum scalar value:", max_location_index_front)
+# print("\tLocation of maximum scalar value:", max_location_front)
+# print("\tMaximum scalar value:", max_value_front)
+
+# contours = outer_mesh.contour() git pull ; rsync -a --exclude=config.toml /home/streamlit/${directory}/* /home/streamlit/
+# https://inside.fraunhofer.de/demo?action=update&apikey=SHA512_HASH_XXXXXX
+# https://inside.fraunhofer.de/transcranial-viewer/bm8?action=update&apikey=SSHA512_HASH_FragilePassw0rd
+def save_for_streamlit(freq, slice_x_focus, contour, slice_y_focus, slice_z_focus, mesh, max_location, max_value, pmin, source_amp, max_location_index):
+ from pathlib import Path
+ root_folder = 'C:/Users/dsinden/GitLab/acoustic-sim-viewer/src/application/input_data/'
+ p = Path(root_folder)
+ p.is_dir()
+ sfreq = str(int(freq / 1e3))
+ slice_x_name = Path(root_folder + 'slice_x_focus_' + sfreq + '.vtk')
+ slice_x_name.is_file()
+ slice_x_focus.save(root_folder + 'slice_x_focus_' + sfreq + '.vtk')
+ slice_y_focus.save(root_folder + 'slice_y_focus_' + sfreq + '.vtk')
+ slice_z_focus.save(root_folder + 'slice_z_focus_' + sfreq + '.vtk')
+ contour.save(root_folder + 'contour_' + sfreq + '.vtk')
+ mesh_name = root_folder + 'mesh_' + sfreq + '.vtk'
+ mesh.save(root_folder + 'mesh_' + sfreq + '.vtk')
+ filename = root_folder + sfreq + 'kHz.npz'
+ np.savez(filename, max_location=max_location, max_value=max_value, min_value=pmin, source_amp=source_amp, max_location_index=max_location_index)
+ print("Saved to:", filename, slice_x_name, mesh_name, freq)
+
+save_for_streamlit(freq, slice_x_focus, contour, slice_y_focus, slice_z_focus, mesh, max_location, max_value, pmin, source_amp, max_location_index)
+
+plotter2 = pv.Plotter()
+plotter2.add_mesh(slice_x_focus, opacity=0.75, cmap='viridis', clim=[pmin, max_value], show_scalar_bar=False)
+
+plotter2.add_mesh(mesh, scalar_bar_args={'title': 'Absolute Pressure [Pa]'}, scalars='abs pressure',
+ opacity=0.25, show_edges=False, cmap='viridis',
+ clim=[pmin, max_value], show_scalar_bar=True)
+
+# plotter2.add_mesh(inner_mesh, scalar_bar_args={'title': 'Absolute Pressure [Pa]'}, scalars='abs pressure',
+# opacity=0.95, show_edges=False, cmap='spring',
+# clim=[pmin, max_value], show_scalar_bar=True)
+
+
+# plotter2.add_mesh(peak_mesh, color='black', label='Contours')
+
+plotter2.add_points(max_location, render_points_as_spheres=True, point_size=10, color='red')
+
+plotter2.add_mesh(contour, color='red', line_width=2, label='half max')
+
+plotter2.view_isometric()
+plotter2.background_color = 'white'
+
+def print_camera_orientation(plotter):
+ # Get the current camera orientation
+ camera = plotter.camera
+ # Print camera position and focal point
+ print("Camera Position:", camera.position)
+ print("Focal Point:", camera.focal_point)
+ print("View Up:", camera.view_up)
+
+# Add a callback for the key press 'r' to print camera orientation
+plotter2.add_key_event('r', lambda: print_camera_orientation(plotter2))
+
+# plotter2.show_grid(axes=True)
+plotter2.show_axes()
+plotter2.show_bounds()
+plotter2.show()
+
diff --git a/examples/benchmarks/8/ph1-bm8-freefield-sc3.py b/examples/benchmarks/8/ph1-bm8-freefield-sc3.py
new file mode 100644
index 000000000..6a2d61c3a
--- /dev/null
+++ b/examples/benchmarks/8/ph1-bm8-freefield-sc3.py
@@ -0,0 +1,1320 @@
+import numpy as np
+
+import logging
+import sys
+import matplotlib.pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+from cycler import cycler
+
+from copy import deepcopy
+
+import h5py
+
+from skimage import measure
+from skimage.segmentation import find_boundaries
+from scipy.interpolate import interpn
+from scipy.interpolate import RegularGridInterpolator
+
+from kwave.data import Vector
+from kwave.utils.kwave_array import kWaveArray
+from kwave.utils.checks import check_stability
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.ksensor import kSensor
+from kwave.utils.signals import create_cw_signals
+from kwave.utils.filters import extract_amp_phase
+from kwave.kspaceFirstOrder3D import kspaceFirstOrder3DG
+
+from kwave.options.simulation_options import SimulationOptions
+from kwave.options.simulation_execution_options import SimulationExecutionOptions
+
+import pyvista as pv
+
+
+# create logger
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+# create console and file handlers and set level to debug
+ch = logging.StreamHandler(sys.stdout)
+ch.setLevel(logging.DEBUG)
+fh = logging.FileHandler(filename='runner.log')
+fh.setLevel(logging.DEBUG)
+
+# create formatter
+formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s')
+# add formatter to ch, fh
+ch.setFormatter(formatter)
+fh.setFormatter(formatter)
+
+# add ch, fh to logger
+logger.addHandler(ch)
+logger.addHandler(fh)
+
+# propagate
+ch.propagate = True
+fh.propagate = True
+logger.propagate = True
+
+verbose: bool = True
+savePlotting: bool = True
+useMaxTimeStep: bool = True
+
+tag = 'bm8'
+res = '1mm'
+transducer = 'sc3'
+
+mask_folder = 'C:/Users/dsinden/GitHub/k-wave-python/data/'
+
+mask_filename = mask_folder + 'skull_mask_' + tag + '_dx_' + res + '.mat'
+
+if verbose:
+ logger.info(mask_filename)
+
+data = h5py.File(mask_filename, 'r')
+
+if verbose:
+ logger.info( list(data.keys()) )
+
+# is given in millimetres
+dx = data['dx'][:].item()
+
+# scale to metres
+dx = dx / 1000.0
+dy = dx
+dz = dx
+
+xi = np.squeeze(np.asarray(data['xi'][:]))
+yi = np.squeeze(np.asarray(data['yi'][:]))
+zi = np.squeeze(np.asarray(data['zi'][:]))
+
+matlab_shape = np.shape(xi)[0], np.shape(yi)[0], np.shape(zi)[0]
+
+skull_mask = np.squeeze(data['skull_mask'][:]).astype(bool)
+brain_mask = np.squeeze(data['brain_mask'][:]).astype(bool)
+
+# convert to Fortran-ordered arrays
+skull_mask = np.reshape(skull_mask.flatten(), matlab_shape, order='F')
+brain_mask = np.reshape(brain_mask.flatten(), matlab_shape, order='F')
+
+# create water mask
+water_mask = np.ones(skull_mask.shape, dtype=int) - (skull_mask.astype(int) +
+ brain_mask.astype(int))
+water_mask = water_mask.astype(bool)
+
+# orientation of axes
+skull_mask = np.swapaxes(skull_mask, 0, 2)
+brain_mask = np.swapaxes(brain_mask, 0, 2)
+water_mask = np.swapaxes(water_mask, 0, 2)
+
+# # cropping settings - was 10
+skull_mask = skull_mask[:, :, 16:]
+brain_mask = brain_mask[:, :, 16:]
+water_mask = water_mask[:, :, 16:]
+
+# set domains sizes
+Nx, Ny, Nz = skull_mask.shape
+
+msg = "new shape=" + str(skull_mask.shape)
+if verbose:
+ logger.info(msg)
+
+if (transducer == 'sc1'):
+ # curved element with focal depth of 64 mm, so is scaled by resolution to give value in grid point
+ # bowl radius of curvature [m]
+ msg = "transducer is focused"
+ focus = int(64 / data['dx'][:].item())
+ focus_coords = [(Nx - 1) // 2, (Ny - 1) // 2, focus]
+ bowl_coords = [(Nx - 1) // 2, (Ny - 1) // 2, 0]
+
+if (transducer == 'sc2'):
+ # planar disc element
+ msg = "transducer is planar disc"
+ focus_coords = [(Nx - 1) // 2, (Ny - 1) // 2, (Nz - 1) // 2]
+ disc_coords = [(Nx - 1) // 2, (Ny - 1) // 2, 0]
+
+if (transducer == 'sc3'):
+ # planar rectangular element
+ msg = "transducer is rectangular"
+ focus_coords = [(Nx - 1) // 2, (Ny - 1) // 2, (Nz - 1) // 2]
+ disc_coords = [(Nx - 1) // 2, (Ny - 1) // 2, 0]
+
+if verbose:
+ logger.info(msg)
+
+# =========================================================================
+# DEFINE THE MATERIAL PROPERTIES
+# =========================================================================
+
+# water
+sound_speed = 1500.0 * np.ones(skull_mask.shape)
+density = 1000.0 * np.ones(skull_mask.shape)
+alpha_coeff = np.zeros(skull_mask.shape)
+
+# non-dispersive
+alpha_power = 2.0
+
+# skull
+sound_speed[skull_mask] = 2800.0
+density[skull_mask] = 1850.0
+alpha_coeff[skull_mask] = 4.0
+
+# brain
+sound_speed[brain_mask] = 1560.0
+density[brain_mask] = 1040.0
+alpha_coeff[brain_mask] = 0.3
+
+c0_min = np.min(sound_speed.flatten())
+c0_max = np.min(sound_speed.flatten())
+
+medium = kWaveMedium(
+ sound_speed=sound_speed,
+ density=density,
+ alpha_coeff=alpha_coeff,
+ alpha_power=alpha_power
+)
+
+# =========================================================================
+# DEFINE THE TRANSDUCER SETUP
+# =========================================================================
+
+# single spherical transducer
+if (transducer == 'sc1'):
+
+ # bowl radius of curvature [m]
+ source_roc = 64.0e-3
+
+ # as we will use the bowl element this has to be a int or float
+ diameters = 64.0e-3
+
+elif (transducer == 'sc2'):
+
+ # diameter of the disc
+ diameter = 10e-3
+
+elif (transducer == 'sc3'):
+
+ # diameter of the disc
+ Lx = 10e-3
+ Ly = 10e-3
+
+# frequency [Hz]
+freq = 400e3
+
+
+# source pressure [Pa]
+source_amp = np.array([60e3])
+
+# phase [rad]
+source_phase = np.array([0.0])
+
+
+# =========================================================================
+# DEFINE COMPUTATIONAL PARAMETERS
+# =========================================================================
+
+# wavelength
+k_min = c0_min / freq
+
+# points per wavelength
+ppw = k_min / dx
+
+# number of periods to record
+record_periods: int = 3
+
+# compute points per period
+ppp: int = 20
+
+# CFL number determines time step
+cfl = (ppw / ppp)
+
+
+# =========================================================================
+# DEFINE THE KGRID
+# =========================================================================
+
+grid_size_points = Vector([Nx, Ny, Nz])
+
+grid_spacing_meters = Vector([dx, dy, dz])
+
+# create the k-space grid
+kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)
+
+
+# =========================================================================
+# DEFINE THE TIME VECTOR
+# =========================================================================
+
+# compute corresponding time stepping
+dt = 1.0 / (ppp * freq)
+
+# compute corresponding time stepping
+dt = (c0_min / c0_max) / (float(ppp) * freq)
+
+dt_stability_limit = check_stability(kgrid, medium)
+msg = "dt_stability_limit=" + str(dt_stability_limit) + ", dt=" + str(dt)
+if verbose:
+ logger.info(msg)
+
+if (useMaxTimeStep and (not np.isfinite(dt_stability_limit)) and
+ (dt_stability_limit < dt)):
+ dt_old = dt
+ ppp = np.ceil( 1.0 / (dt_stability_limit * freq) )
+ dt = 1.0 / (ppp * freq)
+ if verbose:
+ logger.info("updated dt")
+else:
+ if verbose:
+ logger.info("not updated dt")
+
+
+# calculate the number of time steps to reach steady state
+t_end = np.sqrt(kgrid.x_size**2 + kgrid.y_size**2) / c0_min
+
+# create the time array using an integer number of points per period
+Nt = round(t_end / dt)
+
+# make time array
+kgrid.setTime(Nt, dt)
+
+# calculate the actual CFL after adjusting for dt
+cfl_actual = 1.0 / (dt * freq)
+
+if verbose:
+ logger.info('PPW = ' + str(ppw))
+ logger.info('CFL = ' + str(cfl_actual))
+ logger.info('PPP = ' + str(ppp))
+
+
+# =========================================================================
+# DEFINE THE SOURCE PARAMETERS
+# =========================================================================
+
+if verbose:
+ logger.info("kSource")
+
+# create empty kWaveArray this specfies the transducer properties
+karray = kWaveArray(bli_tolerance=0.01,
+ upsampling_rate=16,
+ single_precision=True)
+
+if (transducer == 'sc1'):
+
+ # set bowl position and orientation
+ bowl_pos = [kgrid.x_vec[bowl_coords[0]].item(),
+ kgrid.y_vec[bowl_coords[1]].item(),
+ kgrid.z_vec[bowl_coords[2]].item()]
+
+ focus_pos = [kgrid.x_vec[focus_coords[0]].item(),
+ kgrid.y_vec[focus_coords[1]].item(),
+ kgrid.z_vec[focus_coords[2]].item()]
+
+ # add bowl shaped element
+ karray.add_bowl_element(bowl_pos, source_roc, diameters, focus_pos)
+
+elif (transducer == 'sc2'):
+
+ # set disc position
+ position = [kgrid.x_vec[disc_coords[0]].item(),
+ kgrid.y_vec[disc_coords[1]].item(),
+ kgrid.z_vec[disc_coords[2]].item()]
+
+ # arbitrary position
+ focus_pos = [kgrid.x_vec[focus_coords[0]].item(),
+ kgrid.y_vec[focus_coords[1]].item(),
+ kgrid.z_vec[focus_coords[2]].item()]
+
+ # add disc-shaped planar element
+ karray.add_disc_element(position, diameter, focus_pos)
+
+elif (transducer == 'sc3'):
+ position = [kgrid.x_vec[disc_coords[0]].item(),
+ kgrid.y_vec[disc_coords[1]].item(),
+ kgrid.z_vec[disc_coords[2]].item()]
+ karray.add_rect_element(position=position, Lx=Lx, Ly=Ly, theta=[0,0,0])
+
+# create time varying source
+source_sig = create_cw_signals(np.squeeze(kgrid.t_array),
+ freq,
+ source_amp,
+ source_phase)
+
+# 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)
+
+
+# =========================================================================
+# DEFINE THE SENSOR PARAMETERS
+# =========================================================================
+
+if verbose:
+ logger.info("kSensor")
+
+sensor = kSensor()
+
+# set sensor mask: the mask says at which points data should be recorded
+sensor.mask = np.ones((Nx, Ny, Nz), dtype=bool)
+
+# set the record type: record the pressure waveform
+sensor.record = ['p']
+
+# record 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
+# =========================================================================
+
+DATA_CAST = 'single'
+DATA_PATH = './'
+
+input_filename = tag + '_' + transducer + '_' + res + '_input.h5'
+output_filename = tag + '_' + transducer + '_' + res + '_output.h5'
+
+# set input options
+if verbose:
+ logger.info("simulation_options")
+
+# options for writing to file, but not doing simulations
+simulation_options = SimulationOptions(
+ data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename,
+ output_filename=output_filename,
+ save_to_disk_exit=False,
+ data_path=DATA_PATH,
+ pml_inside=False)
+
+if verbose:
+ logger.info("execution_options")
+
+execution_options = SimulationExecutionOptions(
+ is_gpu_simulation=True,
+ delete_data=False,
+ verbose_level=2)
+
+
+
+# =========================================================================
+# RUN THE SIMULATION
+# =========================================================================
+
+if verbose:
+ logger.info("kspaceFirstOrder3DG")
+
+sensor_data = kspaceFirstOrder3DG(
+ medium=medium,
+ kgrid=kgrid,
+ source=source,
+ sensor=sensor,
+ simulation_options=simulation_options,
+ execution_options=execution_options)
+
+
+# =========================================================================
+# POST-PROCESS
+# =========================================================================
+
+
+# * needs p
+
+if verbose:
+ logger.info("post processing")
+
+# sampling frequency
+fs = 1.0 / kgrid.dt
+
+if verbose:
+ logger.info("extract_amp_phase")
+
+# get Fourier coefficients
+sensor_data['p'].astype(np.csingle)
+amp, _, _ = extract_amp_phase(sensor_data['p'].T, fs, freq, dim=1,
+ fft_padding=1, window='Rectangular')
+
+# reshape data: matlab uses Fortran ordering
+p = np.reshape(amp, (Nx, Ny, Nz), order='F')
+
+x = np.linspace(-Nx // 2, Nx // 2 - 1, Nx)
+y = np.linspace(-Ny // 2, Ny // 2 - 1, Ny)
+z = np.linspace(-Nz // 2, Nz // 2 - 1, Nz)
+x, y, z = np.meshgrid(x, y, z, indexing='ij')
+
+pmax = np.nanmax(p)
+max_loc = np.unravel_index(np.nanargmax(p), p.shape, order='C')
+
+p_water = np.empty_like(p)
+p_water.fill(np.nan)
+p_water[water_mask] = p[water_mask]
+pmax_water = np.nanmax(p_water)
+max_loc_water = np.unravel_index(np.nanargmax(p_water), p.shape, order='C')
+
+p_skull = np.empty_like(p)
+p_skull.fill(np.nan)
+p_skull[skull_mask] = p[skull_mask]
+pmax_skull = np.nanmax(p_skull)
+max_loc_skull = np.unravel_index(np.nanargmax(p_skull), p.shape, order='C')
+
+p_brain = np.empty_like(p)
+p_brain.fill(np.nan)
+p_brain[brain_mask] = p[brain_mask]
+pmax_brain = np.nanmax(p_brain)
+max_loc_brain = np.unravel_index(np.nanargmax(p_brain), p.shape, order='C')
+
+# domain axes
+x_vec = np.linspace(kgrid.x_vec[0].item(), kgrid.x_vec[-1].item(), kgrid.Nx)
+y_vec = np.linspace(kgrid.y_vec[0].item(), kgrid.y_vec[-1].item(), kgrid.Ny)
+z_vec = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+
+# colours
+cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
+
+# brain axes
+# x
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[0]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+elif (transducer == 'sc3'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+x_x = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_brain[2]].item()]
+y_x = [kgrid.x_vec[indy].item(), kgrid.x_vec[max_loc_brain[0]].item()]
+coefficients_x = np.polyfit(x_x, y_x, 1)
+polynomial_x = np.poly1d(coefficients_x)
+axis = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+beam_axis_x = polynomial_x(z_vec)
+# y
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[1]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+elif (transducer == 'sc3'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+x_y = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_brain[2]].item()]
+y_y = [kgrid.y_vec[indy].item(), kgrid.y_vec[max_loc_brain[1]].item()]
+coefficients_y = np.polyfit(x_y, y_y, 1)
+polynomial_y = np.poly1d(coefficients_y)
+beam_axis_y = polynomial_y(z_vec)
+# beam axis
+beam_axis = np.vstack((beam_axis_x, beam_axis_y, z_vec)).T
+# interpolate for pressure on brain axis
+beam_pressure_brain = interpn((x_vec, y_vec, z_vec) , p, beam_axis,
+ method='linear', bounds_error=False, fill_value=np.nan)
+
+# skull axes
+# x
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[0]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+elif (transducer == 'sc3'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+x_x = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_skull[2]].item()]
+y_x = [kgrid.x_vec[indy].item(), kgrid.x_vec[max_loc_skull[0]].item()]
+coefficients_x = np.polyfit(x_x, y_x, 1)
+polynomial_x = np.poly1d(coefficients_x)
+axis = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+beam_axis_x = polynomial_x(z_vec)
+# y
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[1]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+elif (transducer == 'sc3'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+x_y = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_skull[2]].item()]
+y_y = [kgrid.y_vec[indy].item(), kgrid.y_vec[max_loc_skull[1]].item()]
+coefficients_y = np.polyfit(x_y, y_y, 1)
+polynomial_y = np.poly1d(coefficients_y)
+beam_axis_y = polynomial_y(z_vec)
+# beam axis
+beam_axis = np.vstack((beam_axis_x, beam_axis_y, z_vec)).T
+# interpolate for pressure
+beam_pressure_skull = interpn((x_vec, y_vec, z_vec) , p, beam_axis,
+ method='linear', bounds_error=False, fill_value=np.nan)
+
+
+
+# plot pressure on through centre lines
+fig1, ax1 = plt.subplots()
+# ax1.plot(p[(Nx-1)//2, (Nx-1)//2, :] / 1e6, label='geometric')
+ax1.plot(beam_pressure_brain / np.max(beam_pressure_brain))
+ax1.plot(p[focus_coords[0], focus_coords[1], :] / np.max(p))
+# ax1.plot(beam_pressure_skull / 1e6, label='skull')
+# ax1.hlines(pmax_brain / np.max(beam_pressure_brain), 0, len(z_vec), color=cycle[1], linestyle='dashed', lw=0.5)
+# ax1.hlines(pmax_skull / 1e6, 0, len(z_vec), color=cycle[2], linestyle='dashed', lw=0.5)
+ax1.set(xlabel='Axial Position [mm]',
+ ylabel='Pressure []',
+ title='Centreline Pressure')
+ax1.legend()
+ax1.grid(True)
+
+
+
+def get_edges(mask, fill_with_nan=True):
+ """returns the mask as a float array and Np.NaN"""
+ edges = find_boundaries(mask, mode='thin').astype(np.float32)
+ if fill_with_nan:
+ edges[edges == 0] = np.nan
+ return edges
+
+# contouring block
+
+edges_x = get_edges(np.transpose(skull_mask[max_loc_brain[0], :, :]).astype(int), fill_with_nan=False)
+edges_y = get_edges(np.transpose(skull_mask[:, max_loc_brain[1], :]).astype(int), fill_with_nan=False)
+edges_z = get_edges(np.transpose(skull_mask[:, :, max_loc_brain[2]]).astype(int), fill_with_nan=False)
+
+contour_x, num_x = measure.label(edges_x, background=0, return_num=True, connectivity=2)
+contour_y, num_y = measure.label(edges_y, background=0, return_num=True, connectivity=2)
+contour_z, num_z = measure.label(edges_z, background=0, return_num=True, connectivity=2)
+
+if verbose:
+ msg = "size of contours:" + str(np.shape(contour_x)) + ", " + str(np.shape(contour_y)) + ", " + str(np.shape(contour_z)) + "."
+ logger.info(msg)
+ msg = "number of contours: (" + str(num_x) + ", " + str(num_y) + ", " + str(num_z) + ")."
+ logger.info(msg)
+
+jmax = 0
+jmin = Ny
+i_inner = None
+i_outer = None
+# for a number of contours
+for i in range(num_x):
+ idx = int(np.shape(contour_x)[1] // 2)
+ j = np.argmax(np.where(contour_x[:, idx]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i + 1
+ k = np.argmin(np.where(contour_x[:, idx]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i + 1
+contours_x_inner = measure.find_contours(np.where(contour_x==i_inner, 1, 0))
+if not contours_x_inner:
+ logger.warning("size of contours_x_inner is zero")
+contours_x_outer = measure.find_contours(np.where(contour_x==i_outer, 1, 0))
+if not contours_x_outer:
+ logger.warning("size of contours_x_outer is zero")
+inner_index_x = float(Ny)
+outer_index_x = float(0)
+for i in range(len(contours_x_inner)):
+ x_min = np.min(contours_x_inner[i][:, 1])
+ if (x_min < inner_index_x):
+ inner_index_x = i
+for i in range( len(contours_x_outer) ):
+ x_max = np.max(contours_x_outer[i][:, 1])
+ if (x_max > outer_index_x):
+ outer_index_x = i
+
+jmax = 0
+jmin = Nx
+i_inner = None
+i_outer = None
+for i in range(num_y):
+ idy: int = int(np.shape(contour_y)[1] // 2)
+ j = np.argmax(np.where(contour_y[:, idy]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i + 1
+ k = np.argmin(np.where(contour_y[:, idy]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i + 1
+contours_y_inner = measure.find_contours(np.where(contour_y==i_inner, 1, 0))
+if not contours_y_inner:
+ logger.warning("size of contours_y_inner is zero")
+contours_y_outer = measure.find_contours(np.where(contour_y==i_outer, 1, 0))
+if not contours_y_outer:
+ logger.warning("size of contours_y_outer is zero")
+inner_index_y = float(Nx)
+outer_index_y = float(0)
+for i in range( len(contours_y_inner) ):
+ y_min = np.min(contours_y_inner[i][:, 1])
+ if (y_min < inner_index_y):
+ inner_index_y = i
+for i in range( len(contours_y_outer) ):
+ y_max = np.max(contours_y_outer[i][:, 1])
+ if (y_max > outer_index_y):
+ outer_index_y = i
+
+jmax = 0
+jmin = Ny
+i_inner = None
+i_outer = None
+for i in range(num_z):
+ idz: int = int(np.shape(contour_z)[1] // 2)
+ j = np.argmax(np.where(contour_z[:, idz]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i+1
+ k = np.argmin(np.where(contour_z[:, idz]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i+1
+
+contours_z_inner = measure.find_contours(np.where(contour_z==i_inner, 1, 0))
+if not contours_z_inner:
+ logger.warning("size of contours_z_inner is zero")
+else:
+ inner_index_z = float(Nx)
+ for i in range( len(contours_z_inner) ):
+ z_min = np.min(contours_z_inner[i][:, 1])
+ if (z_min < inner_index_z):
+ inner_index_z = i
+
+contours_z_outer = measure.find_contours(np.where(contour_z==i_outer, 1, 0))
+if not contours_z_outer:
+ logger.warning("size of contours_z_outer is zero")
+else:
+ outer_index_z = float(0)
+ for i in range( len(contours_z_outer) ):
+ z_max = np.max(contours_z_outer[i][:, 1])
+ if (z_max > outer_index_z):
+ outer_index_z = i
+
+# end of contouring block
+
+edges_x = get_edges(np.transpose(skull_mask[max_loc_brain[0], :, :]).astype(int))
+edges_y = get_edges(np.transpose(skull_mask[:, max_loc_brain[1], :]).astype(int))
+edges_z = get_edges(np.transpose(skull_mask[:, :, max_loc_brain[2]]).astype(int), fill_with_nan=True)
+
+# plot the pressure field at mid point along z axis
+fig2, ax2 = plt.subplots()
+im2 = ax2.imshow(p[:, :, max_loc_brain[2]] / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='lower',
+ cmap='viridis')
+
+if not contours_z_inner:
+ ax2.imshow(edges_z, aspect='auto', interpolation='none',
+ cmap='Greys', origin='upper')
+else:
+ ax2.plot(contours_z_inner[inner_index_z][:, 1],
+ contours_z_inner[inner_index_z][:, 0], 'w', linewidth=0.5)
+if not contours_z_outer:
+ pass
+else:
+ ax2.plot(contours_z_outer[outer_index_z][:, 1],
+ contours_z_outer[outer_index_z][:, 0], 'w', linewidth=0.5)
+
+ax2.set(xlabel=r'$x$ [mm]',
+ ylabel=r'$y$ [mm]',
+ title='Pressure Field')
+ax2.grid(False)
+divider2 = make_axes_locatable(ax2)
+cax2 = divider2.append_axes("right", size="5%", pad=0.05)
+cbar_2 = fig2.colorbar(im2, cax=cax2)
+cbar_2.ax.set_title('[MPa]', fontsize='small')
+
+pwater_max_x = np.nanmax(p_water[max_loc_brain[0], :, :].flatten())
+pskull_max_x = np.nanmax(p_skull[max_loc_brain[0], :, :].flatten())
+pbrain_max_x = np.nanmax(p_brain[max_loc_brain[0], :, :].flatten())
+
+pwater_max_y = np.nanmax(p_water[:, max_loc_brain[1], :].flatten())
+pskull_max_y = np.nanmax(p_skull[:, max_loc_brain[1], :].flatten())
+pbrain_max_y = np.nanmax(p_brain[:, max_loc_brain[1], :].flatten())
+
+fig3, (ax3a, ax3b) = plt.subplots(1,2)
+im3a_water = ax3a.imshow(p_water[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='cool')
+im3a_skull = ax3a.imshow(p_skull[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='turbo')
+im3a_brain = ax3a.imshow(p_brain[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax3a.plot(contours_x_inner[inner_index_x][:, 1],
+ contours_x_inner[inner_index_x][:, 0], 'k', linewidth=0.5)
+ax3a.plot(contours_x_outer[outer_index_x][:, 1],
+ contours_x_outer[outer_index_x][:, 0], 'k', linewidth=0.5)
+
+ax3a.grid(False)
+ax3a.axes.get_yaxis().set_visible(False)
+ax3a.axes.get_xaxis().set_visible(False)
+divider3a = make_axes_locatable(ax3a)
+cax3a = divider3a.append_axes("right", size="5%", pad=0.05)
+cbar_3a = fig3.colorbar(im3a_brain, cax=cax3a)
+cbar_3a.ax.set_title('[kPa]', fontsize='small')
+ax3b.imshow(p_water[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='cool')
+ax3b.imshow(p_skull[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='turbo')
+im3b_brain = ax3b.imshow(p_brain[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax3b.grid(False)
+ax3b.axes.get_yaxis().set_visible(False)
+ax3b.axes.get_xaxis().set_visible(False)
+divider3b = make_axes_locatable(ax3b)
+cax3b = divider3b.append_axes("right", size="5%", pad=0.05)
+cbar_3b = fig3.colorbar(im3b_brain, cax=cax3b)
+cbar_3b.ax.set_title('[Pa]', fontdict={'fontsize':8})
+
+
+fig4, ax4 = plt.subplots()
+if not contours_z_inner:
+ pass
+else:
+ ax4.plot(contours_z_inner[inner_index_z][:, 1],
+ contours_z_inner[inner_index_z][:, 0], 'w', linewidth=0.5)
+if not contours_z_outer:
+ pass
+else:
+ ax4.plot(contours_z_outer[outer_index_z][:, 1],
+ contours_z_outer[outer_index_z][:, 0], 'w', linewidth=0.5)
+
+
+fig5, (ax5a, ax5b) = plt.subplots(1,2)
+im5a = ax5a.imshow(p[max_loc_brain[0], :, :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+im5a_boundary = ax5a.imshow(edges_x, aspect='auto', interpolation='none',
+ cmap='Greys', origin='upper', alpha=0.75)
+ax5a.grid(False)
+ax5a.axes.get_yaxis().set_visible(False)
+ax5a.axes.get_xaxis().set_visible(False)
+divider5a = make_axes_locatable(ax5a)
+cax5a = divider5a.append_axes("right", size="5%", pad=0.05)
+cbar_5a = fig5.colorbar(im5a, cax=cax5a)
+cbar_5a.ax.set_title('[MPa]', fontsize='small')
+im5b = ax5b.imshow(p[:, max_loc_brain[1], :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+im5b_boundary = ax5b.imshow(edges_y, aspect='auto', interpolation='none',
+ cmap='Greys',origin='upper', alpha=0.75)
+ax5b.grid(False)
+ax5b.axes.get_yaxis().set_visible(False)
+ax5b.axes.get_xaxis().set_visible(False)
+divider5b = make_axes_locatable(ax5b)
+cax5b = divider5b.append_axes("right", size="5%", pad=0.05)
+cbar_5b = fig5.colorbar(im5b, cax=cax5b)
+cbar_5b.ax.set_title('[MPa]', fontsize='small')
+
+all_contours_x = []
+for i in range(num_x):
+ all_contours_x.append(measure.find_contours(np.where(contour_x==(i+1), 1, 0)))
+
+all_contours_y = []
+for i in range(num_y):
+ all_contours_y.append(measure.find_contours(np.where(contour_y==(i+1), 1, 0)))
+
+custom_cycler = cycler(ls=['-', '--', ':', '-.'])
+
+fig6, (ax6a, ax6b) = plt.subplots(1,2)
+
+ax6a.set_prop_cycle(custom_cycler)
+im6a = ax6a.imshow(p[max_loc_brain[0], :, :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+for idx, contour in enumerate(all_contours_x):
+ for i in range( len(contour) ):
+ if ((idx == 0) and (i == 1)) or ((idx == 1) and (i == 0)):
+ ax6a.plot(contour[i][:, 1], contour[i][:, 0], ls='-', c='w',
+ linewidth=0.5)
+ax6a.grid(False)
+ax6a.axes.get_yaxis().set_visible(False)
+ax6a.axes.get_xaxis().set_visible(False)
+divider6a = make_axes_locatable(ax5a)
+cax6a = divider6a.append_axes("right", size="5%", pad=0.05)
+cbar_6a = fig6.colorbar(im6a, cax=cax6a)
+cbar_6a.ax.set_title('[MPa]', fontsize='small')
+im6b = ax6b.imshow(p[:, max_loc_brain[1], :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax6b.set_prop_cycle(custom_cycler)
+for idx, contour in enumerate(all_contours_y):
+ for i in range( len(contour) ):
+ if (idx == 0) and (i==1):
+ ax6b.plot(contour[i][:, 1], contour[i][:, 0], ls='-', c='w',
+ linewidth=0.5)
+# ax6b.legend()
+ax6b.grid(False)
+ax6b.axes.get_yaxis().set_visible(False)
+ax6b.axes.get_xaxis().set_visible(False)
+divider6b = make_axes_locatable(ax6b)
+cax6b = divider6b.append_axes("right", size="5%", pad=0.05)
+cbar_6b = fig6.colorbar(im6b, cax=cax6b)
+cbar_6b.ax.set_title('[MPa]', fontsize='small')
+
+# plt.show()
+
+plotter = pv.Plotter()
+
+pmax = np.nanmax(p)
+pmin = np.nanmin(p)
+
+grid = pv.ImageData()
+grid.dimensions = np.array(p.shape) + 1
+grid.spacing = (1, 1, 1)
+grid.cell_data['pressure'] = np.ravel(p, order="F")
+
+# the focus is in the brain
+xslice_depth = max_loc_brain[0]
+yslice_depth = max_loc_brain[1]
+zslice_depth = max_loc_brain[2]
+
+brain_data = np.nan_to_num(p_brain, nan=0)
+brain_grid = pv.ImageData()
+brain_grid.dimensions = np.array(brain_data.shape) # Set the dimensions of the grid
+brain_grid.spacing = (1, 1, 1)
+brain_grid.point_data['scalars'] = brain_data.flatten(order='F') # Add scalar data
+
+# Find half the maximum value of the scalar field
+brain_max_value = np.max(brain_data)
+isosurface_value = brain_max_value / 2.0
+
+# Extract the isosurface at half the maximum value
+fwhm_contour = brain_grid.contour(isosurfaces=[isosurface_value,])
+
+# Find the index of the maximum value in the filtered scalar values
+brain_max_index = np.argmax(brain_grid.point_data['scalars'])
+
+# find the maximum value
+brain_max_value = brain_grid.point_data['scalars'][brain_max_index]
+
+# max_location_index = filtered_indices[max_index_front]
+brain_max_location = brain_grid.points[brain_max_index]
+
+print(fwhm_contour)
+
+all_regions = fwhm_contour.connectivity('all')
+region_ids = np.unique(all_regions['RegionId'])
+
+noise_region_ids = region_ids[1::] # All region ids except '0'
+others = fwhm_contour.connectivity('specified', noise_region_ids)
+
+largest = fwhm_contour.connectivity('largest')
+
+print(largest.volume)
+
+#----------------------------------
+# fwhm_verts, fwhm_faces, fwhm_normals, fwhm_values = measure.marching_cubes(np.nan_to_num(p_brain, nan=0), pmax_brain / 2.0)
+
+# print("np.shape(fwhm_verts)", np.shape(fwhm_verts))
+
+# import scipy.ndimage as ndi
+
+# # Create a binary mask for the isosurface from marching_cubes
+# binary_mask = np.zeros(p_brain.shape, dtype=bool)
+# for idx, v in enumerate(fwhm_verts):
+# if idx == 0:
+# print(v, int(v[0]), int(v[1]), int(v[2]))
+# binary_mask[int(v[0]), int(v[1]), int(v[2])] = True
+
+# # Label connected components of binary mask
+# labeled_array, num_features = ndi.label(binary_mask)
+
+# print("num_features:", num_features)
+
+# # Prepare to store meshes for each region as a list
+# fwhm_meshes = []
+
+# # Create a dictionary to hold vertices for each component, where zero is not in a mesh
+# region_vertices = {i: [] for i in range(1, num_features + 1)}
+
+# # Associate vertices with their respective regions
+# for index, v in enumerate(fwhm_verts):
+# # Get the corresponding label for the vertex, i.e. not labelled as False=0, but as 1, 2, 3, for number of regions
+# label = labeled_array[int(v[0]), int(v[1]), int(v[2])]
+# if label > 0:
+# # Only consider labeled regions
+# region_vertices[label].append(v)
+
+
+# from collections import defaultdict
+
+# def is_closed(faces):
+# """
+# If verify that every edge in the mesh is shared by exactly two faces.
+# """
+# edge_count = defaultdict(int)
+# # Iterate through each face and count edges
+# for face in faces:
+# # Create edges as tuples of sorted vertex indices to ensure uniqueness
+# for i in range(len(face)):
+# # Get the current vertex and the next vertex (wrap around)
+# edge = (face[i], face[(i + 1) % len(face)])
+# edge = tuple(sorted(edge)) # Sort to handle undirected edges
+# edge_count[edge] += 1
+# # Check if all edges are shared by exactly two faces
+# for count in edge_count.values():
+# if count != 2:
+# return False
+# return True
+
+
+# # Create meshes for each region
+# for label, region_verts in region_vertices.items():
+# print("label:", label)
+# if region_verts:
+# # Check if there are vertices in this region
+# region_verts = np.array(region_verts)
+# region_faces = []
+
+# # Create faces for the current region
+# for face in fwhm_faces:
+# if all(int(fwhm_verts[vert_idx][0]) == int(region_verts[0][0]) for vert_idx in face):
+# region_faces.append(face)
+
+# closed = is_closed(region_faces)
+# print("is closed:", closed)
+
+# # Convert to PyVista mesh and store
+# if region_faces:
+# print("shapes:", np.shape(region_verts), np.shape(np.array(region_faces)))
+# print("region_verts", region_verts)
+# print("region_faces", np.array(region_faces))
+# try:
+# mesh = pv.PolyData(region_verts, np.array(region_faces))
+# fwhm_meshes.append((label, mesh))
+# except:
+# print("error")
+
+# # Sort meshes by volume
+# sorted_meshes = sorted(fwhm_meshes, key=lambda pair: pair[1].volume, reverse=False)
+
+# # Display sorted volumes and meshes
+# for i, mesh in sorted_meshes:
+# print(f"Mesh {i + 1} Volume: {mesh.volume}")
+
+
+#----------------------------------
+
+
+# print(f"Number of connected regions: {num_features}")
+
+# # Extract the coordinates of each region
+# for region in range(1, num_features + 1):
+# coords = np.argwhere(labeled_array == region)
+# print(f"Coordinates of region {region}:")
+# print(coords)
+
+# fwhm_mesh = pv.PolyData(fwhm_verts, fwhm_faces)
+# all_fwhm_regions = fwhm_mesh.connectivity('all')
+# region_ids = np.unique(all_fwhm_regions['RegionId'])
+
+# print("Number of regions:", len(region_ids), region_ids)
+
+# peak_mesh = fwhm_mesh.connectivity('closest', closest_point=max_loc_brain)
+# print(peak_mesh.volume)
+
+# from scipy.spatial import ConvexHull
+
+def fit_ellipsoid(points):
+ # Center the points
+ points = np.array(points)
+ center = np.mean(points, axis=0)
+ points_centered = points - center
+
+ # Create the design matrix
+ D = np.hstack((points_centered, np.ones((points.shape[0], 1))))
+
+ D = np.hstack([points_centered**2, points_centered, np.ones((points.shape[0], 1))])
+
+ # Compute the covariance matrix: Ax^2 + By^2 +Cz^2 +Dxy+Exz+Fyz+Gx+Hy+Iz+J=0
+ covariance_matrix = np.dot(D.T, D)
+
+ # Get the eigenvalues and eigenvectors
+ eigvals, eigvecs = np.linalg.eig(covariance_matrix)
+
+ # Sort eigenvalues and corresponding eigenvectors
+ order = eigvals.argsort()[::-1]
+ eigvals = eigvals[order]
+ eigvecs = eigvecs[:, order]
+
+ # Calculate axes lengths
+ axes_lengths = 1.0 / np.sqrt(eigvals)
+
+ return center, axes_lengths, eigvecs
+
+
+# # Fit ellipsoid
+center, axes_lengths, axes = fit_ellipsoid(largest.points)
+
+print("Center of the ellipsoid:", center)
+print("Axes lengths:", axes_lengths)
+print("Axes directions (eigenvectors):", axes)
+
+
+slice_x_focus = grid.slice(normal='x', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+slice_y_focus = grid.slice(normal='y', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+slice_z_focus = grid.slice(normal='z', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+
+slice_z_tx = grid.slice(normal='-z', origin=disc_coords,
+ generate_triangles=False, contour=False, progress_bar=False)
+
+slice_z_rx = grid.slice(normal='z', origin=[(Nx-1) // 2, (Ny - 1) // 2, Nz-1],
+ generate_triangles=False, contour=False, progress_bar=False)
+
+slice_array = slice_z_rx.cell_data['pressure'].reshape(grid.dimensions[0]-1, grid.dimensions[1]-1)
+
+# now get points on skull surfaces
+verts, faces, normals, _ = measure.marching_cubes(skull_mask, 0)
+
+vfaces = np.column_stack((np.ones(len(faces),) * 3, faces)).astype(int)
+
+x = np.arange(p.shape[0]) # X-coordinates
+y = np.arange(p.shape[1]) # Y-coordinates
+z = np.arange(p.shape[2]) # Z-coordinates
+
+# set up a interpolator
+interpolator = RegularGridInterpolator((x, y, z), p)
+
+# get the pressure values on the vertices
+interpolated_values = interpolator(verts)
+
+# set up mesh for skull surface
+mesh = pv.PolyData(verts, vfaces)
+mesh['Normals'] = normals
+
+# Assign interpolated data to mesh
+mesh.point_data['abs pressure'] = interpolated_values
+
+# clip data
+mesh.point_data['abs pressure'] = np.where(mesh.point_data['abs pressure'] > pmax_brain, pmax_brain, mesh.point_data['abs pressure'] )
+
+if verbose:
+ msg = 'focus in brain: ' + str(max_loc_brain) + ', mid point: ' + str(disc_coords) + ' last plane: ' + str(np.unravel_index(np.argmax(slice_array), slice_array.shape))
+ logger.info(msg)
+
+# Choose a colormap
+plotter.add_mesh(mesh, scalars='abs pressure', opacity=0.25, show_edges=False, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=True)
+plotter.add_mesh(slice_x_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_y_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_tx, opacity=0.75, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_rx, opacity=0.75, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.show_axes()
+plotter.show_bounds()
+
+# plotter.show()
+
+plotter1 = pv.Plotter()
+plotter1.add_mesh(mesh, scalars='abs pressure',
+ opacity=0.25, show_edges=False, cmap='viridis',
+ clim=[pmin, pmax_brain], show_scalar_bar=True, above_color='yellow')
+
+# copy mesh
+back_mesh = deepcopy(mesh)
+front_mesh = deepcopy(mesh)
+
+max_z = mesh.points[:, 2].max()
+half_max_z = max_z / 2.0
+
+condition1 = (mesh.points[:, 2] > half_max_z)
+
+centre = np.array([Nx // 2, Ny // 2, Nz // 2])
+
+
+# Vector from center to each vertex
+vec_from_centre = mesh.points - centre
+
+# Normalize the vectors from center to each vertex
+vec_from_centre_normalized = vec_from_centre / np.linalg.norm(vec_from_centre, axis=1)[:, np.newaxis]
+
+front_mesh.compute_normals()
+
+vec = np.zeros(np.shape(mesh.point_data['Normals'])[0])
+for i in range(np.shape(mesh.point_data['Normals'])[0]):
+ vec[i] = np.dot(mesh.point_data['Normals'][i, :], vec_from_centre_normalized[i, :])
+condition2 = vec > 0.0
+
+filtered_indices_front = np.squeeze(np.where(condition1 & condition2))
+
+# this is the set difference between the indices of the mesh and the indices of the filtered front mesh
+not_in_array = np.setdiff1d(np.arange(0, mesh.points[:, 2].size, 1, dtype=int), filtered_indices_front.astype(int),)
+
+# set data to zero where the condition is not met: i.e not in filtered_indices_front
+front_mesh.point_data['abs pressure'][not_in_array] = 0.0
+
+# Find the index of the maximum value in the filtered scalar values
+max_index = np.argmax(front_mesh.point_data['abs pressure'])
+
+# find the maximum value
+max_value = front_mesh.point_data['abs pressure'][max_index]
+
+# max_location_index = filtered_indices[max_index_front]
+max_location = front_mesh.points[max_index]
+
+
+
+# output to screen
+print("\tIndex of maximum scalar value:", max_index)
+# print("\tIndex of location of maximum scalar value:", max_location_index)
+print("\tLocation of maximum scalar value:", max_location)
+print("\tMaximum scalar value:", max_value)
+print("\tMinimum scalar value:", pmin)
+
+half_max_value = max_value / 2.0
+# get the contour of the half max value in filtered region
+contour = back_mesh.contour([half_max_value])
+print(contour)
+
+# Define a scalar range of the region to extract from filtered region
+peak_range = [half_max_value, max_value]
+# Extract the mesh of the region around the max_value within the range
+peak_mesh = back_mesh.connectivity(extraction_mode='point_seed', variable_input=max_index, scalar_range=peak_range)
+
+# Extract the mesh of the region around the max_value within the range
+# peak_mesh = back_mesh.connectivity(extraction_mode='closest', variable_input=max_location_index, scalar_range=peak_range)
+
+# plotter1.show()
+
+#------------
+# get the mesh on the skull.
+# * clip orientation: x, y or z axis
+# * clip direction: less than / greater than
+# * clip position: half way.
+# * clip value: 0.0
+# * contour value: half of the maximum value in filtered region
+all_regions = mesh.connectivity('all')
+region_ids = np.unique(all_regions['RegionId'])
+
+print("Number of regions:", len(region_ids), region_ids)
+
+
+# outer_mesh = mesh.connectivity('largest')
+# # inner_mesh = mesh.connectivity('specified', region_ids=region_ids[1])
+
+# max_z = outer_mesh.points[:, 2].max()
+# half_max_z = max_z / 2.0
+
+# # mesh has points and point_data. keep all points, but set the data to zero before half_max_z
+
+# filtered_indices_rev = np.where(outer_mesh.points[:, 2] > half_max_z)[0]
+# filtered_scalar_values_rev = outer_mesh.point_data['abs pressure'][filtered_indices_rev]
+# front_mesh.point_data['abs pressure'][filtered_indices_rev] = 0.0
+
+# max_index_front = np.argmax(filtered_scalar_values_front)
+# max_location_index_front = filtered_indices_rev[max_index_front]
+# max_location_front = front_mesh.points[max_location_index_front]
+# max_value_front = filtered_scalar_values[max_index_front]
+
+
+# print("\tIndex of maximum scalar value:", max_index_front)
+# print("\tIndex of location of maximum scalar value:", max_location_index_front)
+# print("\tLocation of maximum scalar value:", max_location_front)
+# print("\tMaximum scalar value:", max_value_front)
+
+# contours = outer_mesh.contour() git pull ; rsync -a --exclude=config.toml /home/streamlit/${directory}/* /home/streamlit/
+# https://inside.fraunhofer.de/demo?action=update&apikey=SHA512_HASH_XXXXXX
+# https://inside.fraunhofer.de/transcranial-viewer/bm8?action=update&apikey=SSHA512_HASH_FragilePassw0rd
+def save_for_streamlit(freq, slice_x_focus, contour, slice_y_focus, slice_z_focus, mesh, max_location, max_value, pmin, source_amp, max_index):
+ from pathlib import Path
+ root_folder = 'C:/Users/dsinden/GitLab/acoustic-sim-viewer/src/application/input_data/'
+ p = Path(root_folder)
+ p.is_dir()
+ sfreq = str(int(freq / 1e3))
+ sc = '_' + str(transducer)
+ slice_x_name = Path(root_folder + 'slice_x_focus_' + sfreq + sc + '.vtk')
+ slice_x_name.is_file()
+ slice_x_focus.save(root_folder + 'slice_x_focus_' + sfreq + sc + '.vtk')
+ slice_y_focus.save(root_folder + 'slice_y_focus_' + sfreq + sc + '.vtk')
+ slice_z_focus.save(root_folder + 'slice_z_focus_' + sfreq + sc + '.vtk')
+ contour.save(root_folder + 'contour_' + sfreq + sc +'.vtk')
+ mesh_name = root_folder + 'mesh_' + sfreq + sc +'.vtk'
+ mesh.save(root_folder + 'mesh_' + sfreq + sc +'.vtk')
+ filename = root_folder + sfreq + sc + 'kHz.npz'
+ np.savez(filename, max_location=max_location, max_value=max_value, min_value=pmin, source_amp=source_amp, max_location_index=max_index)
+ print("Saved to:", filename, slice_x_name, mesh_name, freq)
+
+save_for_streamlit(freq, slice_x_focus, contour, slice_y_focus, slice_z_focus, mesh, max_location, max_value, pmin, source_amp, max_index)
+
+plotter2 = pv.Plotter()
+plotter2.add_mesh(slice_x_focus, opacity=0.75, cmap='viridis', clim=[pmin, max_value], show_scalar_bar=False)
+
+plotter2.add_mesh(mesh, scalar_bar_args={'title': 'Absolute Pressure [Pa]'}, scalars='abs pressure',
+ opacity=0.25, show_edges=False, cmap='viridis',
+ clim=[pmin, max_value], show_scalar_bar=True)
+
+# plotter2.add_mesh(inner_mesh, scalar_bar_args={'title': 'Absolute Pressure [Pa]'}, scalars='abs pressure',
+# opacity=0.95, show_edges=False, cmap='spring',
+# clim=[pmin, max_value], show_scalar_bar=True)
+
+
+# plotter2.add_mesh(peak_mesh, color='black', label='Contours')
+
+plotter2.add_points(max_location, render_points_as_spheres=True, point_size=10, color='red')
+plotter2.add_points(brain_max_location, render_points_as_spheres=True, point_size=10, color='black')
+
+# plotter2.add_mesh(fwhm_meshes[0][1], color='lightblue', show_edges=True, label='FWHM')
+
+plotter2.add_mesh(largest, color='lightblue', show_edges=False, opacity=0.95)
+plotter2.add_mesh(others, color='goldenrod', show_edges=False, opacity=0.95)
+
+plotter2.add_mesh(contour, color='red', line_width=2, label='half max')
+
+plotter2.view_isometric()
+plotter2.background_color = 'white'
+
+def print_camera_orientation(plotter):
+ # Get the current camera orientation
+ camera = plotter.camera
+ # Print camera position and focal point
+ print("Camera Position:", camera.position)
+ print("Focal Point:", camera.focal_point)
+ print("View Up:", camera.view_up)
+
+# Add a callback for the key press 'r' to print camera orientation
+plotter2.add_key_event('r', lambda: print_camera_orientation(plotter2))
+
+# plotter2.show_grid(axes=True)
+plotter2.show_axes()
+plotter2.show_bounds()
+plotter2.show()
+
diff --git a/examples/benchmarks/8/ph1-bm8-sc1.py b/examples/benchmarks/8/ph1-bm8-sc1.py
new file mode 100644
index 000000000..b6ea4bc78
--- /dev/null
+++ b/examples/benchmarks/8/ph1-bm8-sc1.py
@@ -0,0 +1,926 @@
+import numpy as np
+
+import logging
+import sys
+import matplotlib.pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+from cycler import cycler
+
+import h5py
+
+from skimage import measure
+from skimage.segmentation import find_boundaries
+from scipy.interpolate import interpn
+from scipy.interpolate import RegularGridInterpolator
+
+from kwave.data import Vector
+from kwave.utils.kwave_array import kWaveArray
+from kwave.utils.checks import check_stability
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.ksensor import kSensor
+from kwave.utils.signals import create_cw_signals
+from kwave.utils.filters import extract_amp_phase
+from kwave.kspaceFirstOrder3D import kspaceFirstOrder3DG
+
+from kwave.options.simulation_options import SimulationOptions
+from kwave.options.simulation_execution_options import SimulationExecutionOptions
+
+import pyvista as pv
+
+
+# create logger
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+# create console and file handlers and set level to debug
+ch = logging.StreamHandler(sys.stdout)
+ch.setLevel(logging.DEBUG)
+fh = logging.FileHandler(filename='runner.log')
+fh.setLevel(logging.DEBUG)
+
+# create formatter
+formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s')
+# add formatter to ch, fh
+ch.setFormatter(formatter)
+fh.setFormatter(formatter)
+
+# add ch, fh to logger
+logger.addHandler(ch)
+logger.addHandler(fh)
+
+# propagate
+ch.propagate = True
+fh.propagate = True
+logger.propagate = True
+
+verbose: bool = True
+savePlotting: bool = True
+useMaxTimeStep: bool = True
+
+tag = 'bm8'
+res = '1mm'
+transducer = 'sc2'
+
+mask_folder = 'C:/Users/dsinden/Documents/GitLab/k-wave-python/data/'
+
+mask_filename = mask_folder + 'skull_mask_' + tag + '_dx_' + res + '.mat'
+
+if verbose:
+ logger.info(mask_filename)
+
+data = h5py.File(mask_filename, 'r')
+
+if verbose:
+ logger.info( list(data.keys()) )
+
+# is given in millimetres
+dx = data['dx'][:].item()
+
+# scale to metres
+dx = dx / 1000.0
+dy = dx
+dz = dx
+
+xi = np.squeeze(np.asarray(data['xi'][:]))
+yi = np.squeeze(np.asarray(data['yi'][:]))
+zi = np.squeeze(np.asarray(data['zi'][:]))
+
+matlab_shape = np.shape(xi)[0], np.shape(yi)[0], np.shape(zi)[0]
+
+skull_mask = np.squeeze(data['skull_mask'][:]).astype(bool)
+brain_mask = np.squeeze(data['brain_mask'][:]).astype(bool)
+
+# convert to Fortran-ordered arrays
+skull_mask = np.reshape(skull_mask.flatten(), matlab_shape, order='F')
+brain_mask = np.reshape(brain_mask.flatten(), matlab_shape, order='F')
+
+# create water mask
+water_mask = np.ones(skull_mask.shape, dtype=int) - (skull_mask.astype(int) +
+ brain_mask.astype(int))
+water_mask = water_mask.astype(bool)
+
+# orientation of axes
+skull_mask = np.swapaxes(skull_mask, 0, 2)
+brain_mask = np.swapaxes(brain_mask, 0, 2)
+water_mask = np.swapaxes(water_mask, 0, 2)
+
+# cropping settings - was 10
+skull_mask = skull_mask[48:145, 48:145, 16:]
+brain_mask = brain_mask[48:145, 48:145, 16:]
+water_mask = water_mask[48:145, 48:145, 16:]
+
+Nx, Ny, Nz = skull_mask.shape
+
+msg = "new shape=" + str(skull_mask.shape)
+if verbose:
+ logger.info(msg)
+
+if (transducer == 'sc1'):
+ # curved element with focal depth of 64 mm, so is scaled by resolution to give value in grid point
+ # bowl radius of curvature [m]
+ msg = "transducer is focused"
+ focus = int(64 / data['dx'][:].item())
+ focus_coords = [(Nx - 1) // 2, (Ny - 1) // 2, focus]
+ bowl_coords = [(Nx - 1) // 2, (Ny - 1) // 2, 0]
+
+if (transducer == 'sc2'):
+ # planar element
+ msg = "transducer is planar"
+ focus_coords = [(Nx - 1) // 2, (Ny - 1) // 2, (Nz - 1) // 2]
+ disc_coords = [(Nx - 1) // 2, (Ny - 1) // 2, 0]
+
+if verbose:
+ logger.info(msg)
+
+# =========================================================================
+# DEFINE THE MATERIAL PROPERTIES
+# =========================================================================
+
+# water
+sound_speed = 1500.0 * np.ones(skull_mask.shape)
+density = 1000.0 * np.ones(skull_mask.shape)
+alpha_coeff = np.zeros(skull_mask.shape)
+
+# non-dispersive
+alpha_power = 2.0
+
+# skull
+sound_speed[skull_mask] = 2800.0
+density[skull_mask] = 1850.0
+alpha_coeff[skull_mask] = 4.0
+
+# brain
+sound_speed[brain_mask] = 1560.0
+density[brain_mask] = 1040.0
+alpha_coeff[brain_mask] = 0.3
+
+c0_min = np.min(sound_speed.flatten())
+c0_max = np.min(sound_speed.flatten())
+
+medium = kWaveMedium(
+ sound_speed=sound_speed,
+ density=density,
+ alpha_coeff=alpha_coeff,
+ alpha_power=alpha_power
+)
+
+# =========================================================================
+# DEFINE THE TRANSDUCER SETUP
+# =========================================================================
+
+# single spherical transducer
+if (transducer == 'sc1'):
+
+ # bowl radius of curvature [m]
+ source_roc = 64.0e-3
+
+ # as we will use the bowl element this has to be a int or float
+ diameters = 64.0e-3
+
+elif (transducer == 'sc2'):
+
+ # diameter of the disc
+ diameter = 10e-3
+
+# frequency [Hz]
+freq = 800e3
+
+# source pressure [Pa]
+source_amp = np.array([60e3])
+
+# phase [rad]
+source_phase = np.array([0.0])
+
+
+# =========================================================================
+# DEFINE COMPUTATIONAL PARAMETERS
+# =========================================================================
+
+# wavelength
+k_min = c0_min / freq
+
+# points per wavelength
+ppw = k_min / dx
+
+# number of periods to record
+record_periods: int = 3
+
+# compute points per period
+ppp: int = 20
+
+# CFL number determines time step
+cfl = (ppw / ppp)
+
+
+# =========================================================================
+# DEFINE THE KGRID
+# =========================================================================
+
+grid_size_points = Vector([Nx, Ny, Nz])
+
+grid_spacing_meters = Vector([dx, dy, dz])
+
+# create the k-space grid
+kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)
+
+
+# =========================================================================
+# DEFINE THE TIME VECTOR
+# =========================================================================
+
+# compute corresponding time stepping
+dt = 1.0 / (ppp * freq)
+
+# compute corresponding time stepping
+dt = (c0_min / c0_max) / (float(ppp) * freq)
+
+dt_stability_limit = check_stability(kgrid, medium)
+msg = "dt_stability_limit=" + str(dt_stability_limit) + ", dt=" + str(dt)
+if verbose:
+ logger.info(msg)
+
+if (useMaxTimeStep and (not np.isfinite(dt_stability_limit)) and
+ (dt_stability_limit < dt)):
+ dt_old = dt
+ ppp = np.ceil( 1.0 / (dt_stability_limit * freq) )
+ dt = 1.0 / (ppp * freq)
+ if verbose:
+ logger.info("updated dt")
+else:
+ if verbose:
+ logger.info("not updated dt")
+
+
+# calculate the number of time steps to reach steady state
+t_end = np.sqrt(kgrid.x_size**2 + kgrid.y_size**2) / c0_min
+
+# create the time array using an integer number of points per period
+Nt = round(t_end / dt)
+
+# make time array
+kgrid.setTime(Nt, dt)
+
+# calculate the actual CFL after adjusting for dt
+cfl_actual = 1.0 / (dt * freq)
+
+if verbose:
+ logger.info('PPW = ' + str(ppw))
+ logger.info('CFL = ' + str(cfl_actual))
+ logger.info('PPP = ' + str(ppp))
+
+
+# =========================================================================
+# DEFINE THE SOURCE PARAMETERS
+# =========================================================================
+
+if verbose:
+ logger.info("kSource")
+
+# create empty kWaveArray this specfies the transducer properties
+karray = kWaveArray(bli_tolerance=0.01,
+ upsampling_rate=16,
+ single_precision=True)
+
+if (transducer == 'sc1'):
+
+ # set bowl position and orientation
+ bowl_pos = [kgrid.x_vec[bowl_coords[0]].item(),
+ kgrid.y_vec[bowl_coords[1]].item(),
+ kgrid.z_vec[bowl_coords[2]].item()]
+
+ focus_pos = [kgrid.x_vec[focus_coords[0]].item(),
+ kgrid.y_vec[focus_coords[1]].item(),
+ kgrid.z_vec[focus_coords[2]].item()]
+
+ # add bowl shaped element
+ karray.add_bowl_element(bowl_pos, source_roc, diameters, focus_pos)
+
+elif (transducer == 'sc2'):
+
+ # set disc position
+ position = [kgrid.x_vec[disc_coords[0]].item(),
+ kgrid.y_vec[disc_coords[1]].item(),
+ kgrid.z_vec[disc_coords[2]].item()]
+
+ # arbitrary position
+ focus_pos = [kgrid.x_vec[focus_coords[0]].item(),
+ kgrid.y_vec[focus_coords[1]].item(),
+ kgrid.z_vec[focus_coords[2]].item()]
+
+ # add disc-shaped planar element
+ karray.add_disc_element(position, diameter, focus_pos)
+
+# create time varying source
+source_sig = create_cw_signals(np.squeeze(kgrid.t_array),
+ freq,
+ source_amp,
+ source_phase)
+
+# 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)
+
+
+# =========================================================================
+# DEFINE THE SENSOR PARAMETERS
+# =========================================================================
+
+if verbose:
+ logger.info("kSensor")
+
+sensor = kSensor()
+
+# set sensor mask: the mask says at which points data should be recorded
+sensor.mask = np.ones((Nx, Ny, Nz), dtype=bool)
+
+# set the record type: record the pressure waveform
+sensor.record = ['p']
+
+# record 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
+# =========================================================================
+
+DATA_CAST = 'single'
+DATA_PATH = './'
+
+input_filename = tag + '_' + transducer + '_' + res + '_input.h5'
+output_filename = tag + '_' + transducer + '_' + res + '_output.h5'
+
+# set input options
+if verbose:
+ logger.info("simulation_options")
+
+# options for writing to file, but not doing simulations
+simulation_options = SimulationOptions(
+ data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename,
+ output_filename=output_filename,
+ save_to_disk_exit=False,
+ data_path=DATA_PATH,
+ pml_inside=False)
+
+if verbose:
+ logger.info("execution_options")
+
+execution_options = SimulationExecutionOptions(
+ is_gpu_simulation=True,
+ delete_data=False,
+ verbose_level=2)
+
+
+
+# =========================================================================
+# RUN THE SIMULATION
+# =========================================================================
+
+if verbose:
+ logger.info("kspaceFirstOrder3DG")
+
+sensor_data = kspaceFirstOrder3DG(
+ medium=medium,
+ kgrid=kgrid,
+ source=source,
+ sensor=sensor,
+ simulation_options=simulation_options,
+ execution_options=execution_options)
+
+
+# =========================================================================
+# POST-PROCESS
+# =========================================================================
+
+if verbose:
+ logger.info("post processing")
+
+# sampling frequency
+fs = 1.0 / kgrid.dt
+
+if verbose:
+ logger.info("extract_amp_phase")
+
+# get Fourier coefficients
+amp, _, _ = extract_amp_phase(sensor_data['p'].T, fs, freq, dim=1,
+ fft_padding=1, window='Rectangular')
+
+# reshape data: matlab uses Fortran ordering
+p = np.reshape(amp, (Nx, Ny, Nz), order='F')
+
+x = np.linspace(-Nx // 2, Nx // 2 - 1, Nx)
+y = np.linspace(-Ny // 2, Ny // 2 - 1, Ny)
+z = np.linspace(-Nz // 2, Nz // 2 - 1, Nz)
+x, y, z = np.meshgrid(x, y, z, indexing='ij')
+
+pmax = np.nanmax(p)
+max_loc = np.unravel_index(np.nanargmax(p), p.shape, order='C')
+
+p_water = np.empty_like(p)
+p_water.fill(np.nan)
+p_water[water_mask] = p[water_mask]
+pmax_water = np.nanmax(p_water)
+max_loc_water = np.unravel_index(np.nanargmax(p_water), p.shape, order='C')
+
+p_skull = np.empty_like(p)
+p_skull.fill(np.nan)
+p_skull[skull_mask] = p[skull_mask]
+pmax_skull = np.nanmax(p_skull)
+max_loc_skull = np.unravel_index(np.nanargmax(p_skull), p.shape, order='C')
+
+p_brain = np.empty_like(p)
+p_brain.fill(np.nan)
+p_brain[brain_mask] = p[brain_mask]
+pmax_brain = np.nanmax(p_brain)
+max_loc_brain = np.unravel_index(np.nanargmax(p_brain), p.shape, order='C')
+
+# domain axes
+x_vec = np.linspace(kgrid.x_vec[0].item(), kgrid.x_vec[-1].item(), kgrid.Nx)
+y_vec = np.linspace(kgrid.y_vec[0].item(), kgrid.y_vec[-1].item(), kgrid.Ny)
+z_vec = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+
+# colours
+cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
+
+# brain axes
+# x
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[0]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+x_x = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_brain[2]].item()]
+y_x = [kgrid.x_vec[indy].item(), kgrid.x_vec[max_loc_brain[0]].item()]
+coefficients_x = np.polyfit(x_x, y_x, 1)
+polynomial_x = np.poly1d(coefficients_x)
+axis = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+beam_axis_x = polynomial_x(z_vec)
+# y
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[1]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+x_y = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_brain[2]].item()]
+y_y = [kgrid.y_vec[indy].item(), kgrid.y_vec[max_loc_brain[1]].item()]
+coefficients_y = np.polyfit(x_y, y_y, 1)
+polynomial_y = np.poly1d(coefficients_y)
+beam_axis_y = polynomial_y(z_vec)
+# beam axis
+beam_axis = np.vstack((beam_axis_x, beam_axis_y, z_vec)).T
+# interpolate for pressure on brain axis
+beam_pressure_brain = interpn((x_vec, y_vec, z_vec) , p, beam_axis,
+ method='linear', bounds_error=False, fill_value=np.nan)
+
+# skull axes
+# x
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[0]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+x_x = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_skull[2]].item()]
+y_x = [kgrid.x_vec[indy].item(), kgrid.x_vec[max_loc_skull[0]].item()]
+coefficients_x = np.polyfit(x_x, y_x, 1)
+polynomial_x = np.poly1d(coefficients_x)
+axis = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+beam_axis_x = polynomial_x(z_vec)
+# y
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[1]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+x_y = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_skull[2]].item()]
+y_y = [kgrid.y_vec[indy].item(), kgrid.y_vec[max_loc_skull[1]].item()]
+coefficients_y = np.polyfit(x_y, y_y, 1)
+polynomial_y = np.poly1d(coefficients_y)
+beam_axis_y = polynomial_y(z_vec)
+
+# beam axis
+beam_axis = np.vstack((beam_axis_x, beam_axis_y, z_vec)).T
+
+# interpolate for pressure
+beam_pressure_skull = interpn((x_vec, y_vec, z_vec) , p, beam_axis,
+ method='linear', bounds_error=False, fill_value=np.nan)
+
+
+
+# plot pressure on through centre lines
+fig1, ax1 = plt.subplots()
+ax1.plot(p[(Nx-1)//2, (Nx-1)//2, :] / 1e6, label='geometric')
+ax1.plot(beam_pressure_brain / 1e6, label='focal')
+ax1.plot(beam_pressure_skull / 1e6, label='skull')
+ax1.hlines(pmax_brain / 1e6, 0, len(z_vec), color=cycle[1], linestyle='dashed', lw=0.5)
+ax1.hlines(pmax_skull / 1e6, 0, len(z_vec), color=cycle[2], linestyle='dashed', lw=0.5)
+ax1.set(xlabel='Axial Position [mm]',
+ ylabel='Pressure [MPa]',
+ title='Centreline Pressures')
+ax1.legend()
+ax1.grid(True)
+
+
+
+def get_edges(mask, fill_with_nan=True):
+ """returns the mask as a float array and Np.NaN"""
+ edges = find_boundaries(mask, mode='thin').astype(np.float32)
+ if fill_with_nan:
+ edges[edges == 0] = np.nan
+ return edges
+
+# contouring block
+
+edges_x = get_edges(np.transpose(skull_mask[max_loc_brain[0], :, :]).astype(int), fill_with_nan=False)
+edges_y = get_edges(np.transpose(skull_mask[:, max_loc_brain[1], :]).astype(int), fill_with_nan=False)
+edges_z = get_edges(np.transpose(skull_mask[:, :, max_loc_brain[2]]).astype(int), fill_with_nan=False)
+
+contour_x, num_x = measure.label(edges_x, background=0, return_num=True, connectivity=2)
+contour_y, num_y = measure.label(edges_y, background=0, return_num=True, connectivity=2)
+contour_z, num_z = measure.label(edges_z, background=0, return_num=True, connectivity=2)
+
+if verbose:
+ msg = "size of contours:" + str(np.shape(contour_x)) + ", " + str(np.shape(contour_y)) + ", " + str(np.shape(contour_z)) + "."
+ logger.info(msg)
+ msg = "number of contours: (" + str(num_x) + ", " + str(num_y) + ", " + str(num_z) + ")."
+ logger.info(msg)
+
+jmax = 0
+jmin = Ny
+i_inner = None
+i_outer = None
+# for a number of contours
+for i in range(num_x):
+ idx = int(np.shape(contour_x)[1] // 2)
+ j = np.argmax(np.where(contour_x[:, idx]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i + 1
+ k = np.argmin(np.where(contour_x[:, idx]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i + 1
+contours_x_inner = measure.find_contours(np.where(contour_x==i_inner, 1, 0))
+if not contours_x_inner:
+ logger.warning("size of contours_x_inner is zero")
+contours_x_outer = measure.find_contours(np.where(contour_x==i_outer, 1, 0))
+if not contours_x_outer:
+ logger.warning("size of contours_x_outer is zero")
+inner_index_x = float(Ny)
+outer_index_x = float(0)
+for i in range(len(contours_x_inner)):
+ x_min = np.min(contours_x_inner[i][:, 1])
+ if (x_min < inner_index_x):
+ inner_index_x = i
+for i in range( len(contours_x_outer) ):
+ x_max = np.max(contours_x_outer[i][:, 1])
+ if (x_max > outer_index_x):
+ outer_index_x = i
+
+jmax = 0
+jmin = Nx
+i_inner = None
+i_outer = None
+for i in range(num_y):
+ idy: int = int(np.shape(contour_y)[1] // 2)
+ j = np.argmax(np.where(contour_y[:, idy]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i + 1
+ k = np.argmin(np.where(contour_y[:, idy]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i + 1
+contours_y_inner = measure.find_contours(np.where(contour_y==i_inner, 1, 0))
+if not contours_y_inner:
+ logger.warning("size of contours_y_inner is zero")
+contours_y_outer = measure.find_contours(np.where(contour_y==i_outer, 1, 0))
+if not contours_y_outer:
+ logger.warning("size of contours_y_outer is zero")
+inner_index_y = float(Nx)
+outer_index_y = float(0)
+for i in range( len(contours_y_inner) ):
+ y_min = np.min(contours_y_inner[i][:, 1])
+ if (y_min < inner_index_y):
+ inner_index_y = i
+for i in range( len(contours_y_outer) ):
+ y_max = np.max(contours_y_outer[i][:, 1])
+ if (y_max > outer_index_y):
+ outer_index_y = i
+
+jmax = 0
+jmin = Ny
+i_inner = None
+i_outer = None
+for i in range(num_z):
+ idz: int = int(np.shape(contour_z)[1] // 2)
+ j = np.argmax(np.where(contour_z[:, idz]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i+1
+ k = np.argmin(np.where(contour_z[:, idz]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i+1
+
+contours_z_inner = measure.find_contours(np.where(contour_z==i_inner, 1, 0))
+if not contours_z_inner:
+ logger.warning("size of contours_z_inner is zero")
+else:
+ inner_index_z = float(Nx)
+ for i in range( len(contours_z_inner) ):
+ z_min = np.min(contours_z_inner[i][:, 1])
+ if (z_min < inner_index_z):
+ inner_index_z = i
+
+contours_z_outer = measure.find_contours(np.where(contour_z==i_outer, 1, 0))
+if not contours_z_outer:
+ logger.warning("size of contours_z_outer is zero")
+else:
+ outer_index_z = float(0)
+ for i in range( len(contours_z_outer) ):
+ z_max = np.max(contours_z_outer[i][:, 1])
+ if (z_max > outer_index_z):
+ outer_index_z = i
+
+# end of contouring block
+
+edges_x = get_edges(np.transpose(skull_mask[max_loc_brain[0], :, :]).astype(int))
+edges_y = get_edges(np.transpose(skull_mask[:, max_loc_brain[1], :]).astype(int))
+edges_z = get_edges(np.transpose(skull_mask[:, :, max_loc_brain[2]]).astype(int), fill_with_nan=True)
+
+# plot the pressure field at mid point along z axis
+fig2, ax2 = plt.subplots()
+im2 = ax2.imshow(p[:, :, max_loc_brain[2]] / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='lower',
+ cmap='viridis')
+
+if not contours_z_inner:
+ ax2.imshow(edges_z, aspect='auto', interpolation='none',
+ cmap='Greys', origin='upper')
+else:
+ ax2.plot(contours_z_inner[inner_index_z][:, 1],
+ contours_z_inner[inner_index_z][:, 0], 'w', linewidth=0.5)
+if not contours_z_outer:
+ pass
+else:
+ ax2.plot(contours_z_outer[outer_index_z][:, 1],
+ contours_z_outer[outer_index_z][:, 0], 'w', linewidth=0.5)
+
+ax2.set(xlabel=r'$x$ [mm]',
+ ylabel=r'$y$ [mm]',
+ title='Pressure Field')
+ax2.grid(False)
+divider2 = make_axes_locatable(ax2)
+cax2 = divider2.append_axes("right", size="5%", pad=0.05)
+cbar_2 = fig2.colorbar(im2, cax=cax2)
+cbar_2.ax.set_title('[MPa]', fontsize='small')
+
+pwater_max_x = np.nanmax(p_water[max_loc_brain[0], :, :].flatten())
+pskull_max_x = np.nanmax(p_skull[max_loc_brain[0], :, :].flatten())
+pbrain_max_x = np.nanmax(p_brain[max_loc_brain[0], :, :].flatten())
+
+pwater_max_y = np.nanmax(p_water[:, max_loc_brain[1], :].flatten())
+pskull_max_y = np.nanmax(p_skull[:, max_loc_brain[1], :].flatten())
+pbrain_max_y = np.nanmax(p_brain[:, max_loc_brain[1], :].flatten())
+
+fig3, (ax3a, ax3b) = plt.subplots(1,2)
+im3a_water = ax3a.imshow(p_water[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='cool')
+im3a_skull = ax3a.imshow(p_skull[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='turbo')
+im3a_brain = ax3a.imshow(p_brain[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax3a.plot(contours_x_inner[inner_index_x][:, 1],
+ contours_x_inner[inner_index_x][:, 0], 'k', linewidth=0.5)
+ax3a.plot(contours_x_outer[outer_index_x][:, 1],
+ contours_x_outer[outer_index_x][:, 0], 'k', linewidth=0.5)
+
+ax3a.grid(False)
+ax3a.axes.get_yaxis().set_visible(False)
+ax3a.axes.get_xaxis().set_visible(False)
+divider3a = make_axes_locatable(ax3a)
+cax3a = divider3a.append_axes("right", size="5%", pad=0.05)
+cbar_3a = fig3.colorbar(im3a_brain, cax=cax3a)
+cbar_3a.ax.set_title('[kPa]', fontsize='small')
+ax3b.imshow(p_water[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='cool')
+ax3b.imshow(p_skull[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='turbo')
+im3b_brain = ax3b.imshow(p_brain[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax3b.grid(False)
+ax3b.axes.get_yaxis().set_visible(False)
+ax3b.axes.get_xaxis().set_visible(False)
+divider3b = make_axes_locatable(ax3b)
+cax3b = divider3b.append_axes("right", size="5%", pad=0.05)
+cbar_3b = fig3.colorbar(im3b_brain, cax=cax3b)
+cbar_3b.ax.set_title('[Pa]', fontdict={'fontsize':8})
+
+
+fig4, ax4 = plt.subplots()
+if not contours_z_inner:
+ pass
+else:
+ ax4.plot(contours_z_inner[inner_index_z][:, 1],
+ contours_z_inner[inner_index_z][:, 0], 'w', linewidth=0.5)
+if not contours_z_outer:
+ pass
+else:
+ ax4.plot(contours_z_outer[outer_index_z][:, 1],
+ contours_z_outer[outer_index_z][:, 0], 'w', linewidth=0.5)
+
+
+fig5, (ax5a, ax5b) = plt.subplots(1,2)
+im5a = ax5a.imshow(p[max_loc_brain[0], :, :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+im5a_boundary = ax5a.imshow(edges_x, aspect='auto', interpolation='none',
+ cmap='Greys', origin='upper', alpha=0.75)
+ax5a.grid(False)
+ax5a.axes.get_yaxis().set_visible(False)
+ax5a.axes.get_xaxis().set_visible(False)
+divider5a = make_axes_locatable(ax5a)
+cax5a = divider5a.append_axes("right", size="5%", pad=0.05)
+cbar_5a = fig5.colorbar(im5a, cax=cax5a)
+cbar_5a.ax.set_title('[MPa]', fontsize='small')
+im5b = ax5b.imshow(p[:, max_loc_brain[1], :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+im5b_boundary = ax5b.imshow(edges_y, aspect='auto', interpolation='none',
+ cmap='Greys',origin='upper', alpha=0.75)
+ax5b.grid(False)
+ax5b.axes.get_yaxis().set_visible(False)
+ax5b.axes.get_xaxis().set_visible(False)
+divider5b = make_axes_locatable(ax5b)
+cax5b = divider5b.append_axes("right", size="5%", pad=0.05)
+cbar_5b = fig5.colorbar(im5b, cax=cax5b)
+cbar_5b.ax.set_title('[MPa]', fontsize='small')
+
+all_contours_x = []
+for i in range(num_x):
+ all_contours_x.append(measure.find_contours(np.where(contour_x==(i+1), 1, 0)))
+
+all_contours_y = []
+for i in range(num_y):
+ all_contours_y.append(measure.find_contours(np.where(contour_y==(i+1), 1, 0)))
+
+fig6, (ax6a, ax6b) = plt.subplots(1,2)
+im6a = ax6a.imshow(p[max_loc_brain[0], :, :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+for contour in all_contours_x:
+ # logger.info(contour dir(contour))
+ for i in range( len(contour) ):
+ ax6a.plot(contour[i][:, 1], contour[i][:, 0], 'w', linewidth=0.5)
+
+ax6a.grid(False)
+ax6a.axes.get_yaxis().set_visible(False)
+ax6a.axes.get_xaxis().set_visible(False)
+divider6a = make_axes_locatable(ax5a)
+cax6a = divider6a.append_axes("right", size="5%", pad=0.05)
+cbar_6a = fig6.colorbar(im6a, cax=cax6a)
+cbar_6a.ax.set_title('[MPa]', fontsize='small')
+im6b = ax6b.imshow(p[:, max_loc_brain[1], :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+custom_cycler = cycler(ls=['-', '--', ':', '-.'])
+
+ax6b.set_prop_cycle(custom_cycler)
+
+for idx, contour in enumerate(all_contours_y):
+ for i in range( len(contour) ):
+ ax6b.plot(contour[i][:, 1], contour[i][:, 0], c=cycle[idx],
+ linewidth=0.5, label=str(idx))
+ax6b.legend()
+ax6b.grid(False)
+ax6b.axes.get_yaxis().set_visible(False)
+ax6b.axes.get_xaxis().set_visible(False)
+divider6b = make_axes_locatable(ax6b)
+cax6b = divider6b.append_axes("right", size="5%", pad=0.05)
+cbar_6b = fig6.colorbar(im6b, cax=cax6b)
+cbar_6b.ax.set_title('[MPa]', fontsize='small')
+
+# plt.show()
+
+plotter = pv.Plotter()
+
+pmax = np.nanmax(p)
+pmin = np.nanmin(p)
+
+grid = pv.ImageData()
+grid.dimensions = np.array(p.shape) + 1
+grid.spacing = (1, 1, 1)
+grid.cell_data['pressure'] = np.ravel(p, order="F")
+
+xslice_depth = max_loc_brain[0]
+yslice_depth = max_loc_brain[1]
+zslice_depth = max_loc_brain[2]
+
+
+
+slice_x_focus = grid.slice(normal='x', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+slice_y_focus = grid.slice(normal='y', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+slice_z_focus = grid.slice(normal='z', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+
+# slice_array = slice_z_focus.cell_data['pressure'].reshape(grid.dimensions[0]-1, grid.dimensions[1]-1)
+
+slice_z_tx = grid.slice(normal='-z', origin=disc_coords,
+ generate_triangles=False, contour=False, progress_bar=False)
+
+# slice_array = slice_z_tx.cell_data['pressure'].reshape(grid.dimensions[0]-1, grid.dimensions[1]-1)
+
+slice_z_rx = grid.slice(normal='z', origin=[(Nx-1) // 2, (Ny - 1) // 2, Nz-1],
+ generate_triangles=False, contour=False, progress_bar=False)
+
+slice_array = slice_z_rx.cell_data['pressure'].reshape(grid.dimensions[0]-1, grid.dimensions[1]-1)
+
+# now get points on skull surfaces
+verts, faces, normals, _ = measure.marching_cubes(skull_mask, 0)
+
+vfaces = np.column_stack((np.ones(len(faces),) * 3, faces)).astype(int)
+
+x = np.arange(p.shape[0]) # X-coordinates
+y = np.arange(p.shape[1]) # Y-coordinates
+z = np.arange(p.shape[2]) # Z-coordinates
+
+# set up a interpolator
+interpolator = RegularGridInterpolator((x, y, z), p)
+# get the pressure values on the vertices
+interpolated_values = interpolator(verts)
+
+# set up mesh for skull surface
+mesh = pv.PolyData(verts, vfaces)
+mesh['Normals'] = normals
+
+# Assign interpolated data to mesh
+mesh.point_data['abs pressure'] = interpolated_values
+# clip data
+mesh.point_data['abs pressure'] = np.where(mesh.point_data['abs pressure'] > pmax_brain, pmax_brain, mesh.point_data['abs pressure'] )
+
+if verbose:
+ msg = 'focus in brain: ' + str(max_loc_brain) + ', mid point: ' + str(disc_coords) + ' last plane: ' + str(np.unravel_index(np.argmax(slice_array), slice_array.shape))
+ logger.info(msg)
+
+# Choose a colormap
+plotter.add_mesh(mesh, scalars='abs pressure', opacity=0.25, show_edges=False, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=True)
+plotter.add_mesh(slice_x_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_y_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_tx, opacity=0.75, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_rx, opacity=0.75, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.show_axes()
+plotter.show_bounds()
+
+plotter.show()
diff --git a/examples/benchmarks/8/ph1-bm8-sc2.py b/examples/benchmarks/8/ph1-bm8-sc2.py
new file mode 100644
index 000000000..334a927fc
--- /dev/null
+++ b/examples/benchmarks/8/ph1-bm8-sc2.py
@@ -0,0 +1,926 @@
+import numpy as np
+
+import logging
+import sys
+import matplotlib.pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+from cycler import cycler
+
+import h5py
+
+from skimage import measure
+from skimage.segmentation import find_boundaries
+from scipy.interpolate import interpn
+from scipy.interpolate import RegularGridInterpolator
+
+from kwave.data import Vector
+from kwave.utils.kwave_array import kWaveArray
+from kwave.utils.checks import check_stability
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.ksensor import kSensor
+from kwave.utils.signals import create_cw_signals
+from kwave.utils.filters import extract_amp_phase
+from kwave.kspaceFirstOrder3D import kspaceFirstOrder3DG
+
+from kwave.options.simulation_options import SimulationOptions
+from kwave.options.simulation_execution_options import SimulationExecutionOptions
+
+import pyvista as pv
+
+
+# create logger
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+# create console and file handlers and set level to debug
+ch = logging.StreamHandler(sys.stdout)
+ch.setLevel(logging.DEBUG)
+fh = logging.FileHandler(filename='runner.log')
+fh.setLevel(logging.DEBUG)
+
+# create formatter
+formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s')
+# add formatter to ch, fh
+ch.setFormatter(formatter)
+fh.setFormatter(formatter)
+
+# add ch, fh to logger
+logger.addHandler(ch)
+logger.addHandler(fh)
+
+# propagate
+ch.propagate = True
+fh.propagate = True
+logger.propagate = True
+
+verbose: bool = True
+savePlotting: bool = True
+useMaxTimeStep: bool = True
+
+tag = 'bm8'
+res = '1mm'
+transducer = 'sc2'
+
+mask_folder = 'C:/Users/dsinden/GitHub/k-wave-python/data/'
+
+mask_filename = mask_folder + 'skull_mask_' + tag + '_dx_' + res + '.mat'
+
+if verbose:
+ logger.info(mask_filename)
+
+data = h5py.File(mask_filename, 'r')
+
+if verbose:
+ logger.info( list(data.keys()) )
+
+# is given in millimetres
+dx = data['dx'][:].item()
+
+# scale to metres
+dx = dx / 1000.0
+dy = dx
+dz = dx
+
+xi = np.squeeze(np.asarray(data['xi'][:]))
+yi = np.squeeze(np.asarray(data['yi'][:]))
+zi = np.squeeze(np.asarray(data['zi'][:]))
+
+matlab_shape = np.shape(xi)[0], np.shape(yi)[0], np.shape(zi)[0]
+
+skull_mask = np.squeeze(data['skull_mask'][:]).astype(bool)
+brain_mask = np.squeeze(data['brain_mask'][:]).astype(bool)
+
+# convert to Fortran-ordered arrays
+skull_mask = np.reshape(skull_mask.flatten(), matlab_shape, order='F')
+brain_mask = np.reshape(brain_mask.flatten(), matlab_shape, order='F')
+
+# create water mask
+water_mask = np.ones(skull_mask.shape, dtype=int) - (skull_mask.astype(int) +
+ brain_mask.astype(int))
+water_mask = water_mask.astype(bool)
+
+# orientation of axes
+skull_mask = np.swapaxes(skull_mask, 0, 2)
+brain_mask = np.swapaxes(brain_mask, 0, 2)
+water_mask = np.swapaxes(water_mask, 0, 2)
+
+# cropping settings - was 10
+skull_mask = skull_mask[48:145, 48:145, 16:]
+brain_mask = brain_mask[48:145, 48:145, 16:]
+water_mask = water_mask[48:145, 48:145, 16:]
+
+Nx, Ny, Nz = skull_mask.shape
+
+msg = "new shape=" + str(skull_mask.shape)
+if verbose:
+ logger.info(msg)
+
+if (transducer == 'sc1'):
+ # curved element with focal depth of 64 mm, so is scaled by resolution to give value in grid point
+ # bowl radius of curvature [m]
+ msg = "transducer is focused"
+ focus = int(64 / data['dx'][:].item())
+ focus_coords = [(Nx - 1) // 2, (Ny - 1) // 2, focus]
+ bowl_coords = [(Nx - 1) // 2, (Ny - 1) // 2, 0]
+
+if (transducer == 'sc2'):
+ # planar element
+ msg = "transducer is planar"
+ focus_coords = [(Nx - 1) // 2, (Ny - 1) // 2, (Nz - 1) // 2]
+ disc_coords = [(Nx - 1) // 2, (Ny - 1) // 2, 0]
+
+if verbose:
+ logger.info(msg)
+
+# =========================================================================
+# DEFINE THE MATERIAL PROPERTIES
+# =========================================================================
+
+# water
+sound_speed = 1500.0 * np.ones(skull_mask.shape)
+density = 1000.0 * np.ones(skull_mask.shape)
+alpha_coeff = np.zeros(skull_mask.shape)
+
+# non-dispersive
+alpha_power = 2.0
+
+# skull
+sound_speed[skull_mask] = 2800.0
+density[skull_mask] = 1850.0
+alpha_coeff[skull_mask] = 4.0
+
+# brain
+sound_speed[brain_mask] = 1560.0
+density[brain_mask] = 1040.0
+alpha_coeff[brain_mask] = 0.3
+
+c0_min = np.min(sound_speed.flatten())
+c0_max = np.min(sound_speed.flatten())
+
+medium = kWaveMedium(
+ sound_speed=sound_speed,
+ density=density,
+ alpha_coeff=alpha_coeff,
+ alpha_power=alpha_power
+)
+
+# =========================================================================
+# DEFINE THE TRANSDUCER SETUP
+# =========================================================================
+
+# single spherical transducer
+if (transducer == 'sc1'):
+
+ # bowl radius of curvature [m]
+ source_roc = 64.0e-3
+
+ # as we will use the bowl element this has to be a int or float
+ diameters = 64.0e-3
+
+elif (transducer == 'sc2'):
+
+ # diameter of the disc
+ diameter = 10e-3
+
+# frequency [Hz]
+freq = 500e3
+
+# source pressure [Pa]
+source_amp = np.array([60e3])
+
+# phase [rad]
+source_phase = np.array([0.0])
+
+
+# =========================================================================
+# DEFINE COMPUTATIONAL PARAMETERS
+# =========================================================================
+
+# wavelength
+k_min = c0_min / freq
+
+# points per wavelength
+ppw = k_min / dx
+
+# number of periods to record
+record_periods: int = 3
+
+# compute points per period
+ppp: int = 20
+
+# CFL number determines time step
+cfl = (ppw / ppp)
+
+
+# =========================================================================
+# DEFINE THE KGRID
+# =========================================================================
+
+grid_size_points = Vector([Nx, Ny, Nz])
+
+grid_spacing_meters = Vector([dx, dy, dz])
+
+# create the k-space grid
+kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)
+
+
+# =========================================================================
+# DEFINE THE TIME VECTOR
+# =========================================================================
+
+# compute corresponding time stepping
+dt = 1.0 / (ppp * freq)
+
+# compute corresponding time stepping
+dt = (c0_min / c0_max) / (float(ppp) * freq)
+
+dt_stability_limit = check_stability(kgrid, medium)
+msg = "dt_stability_limit=" + str(dt_stability_limit) + ", dt=" + str(dt)
+if verbose:
+ logger.info(msg)
+
+if (useMaxTimeStep and (not np.isfinite(dt_stability_limit)) and
+ (dt_stability_limit < dt)):
+ dt_old = dt
+ ppp = np.ceil( 1.0 / (dt_stability_limit * freq) )
+ dt = 1.0 / (ppp * freq)
+ if verbose:
+ logger.info("updated dt")
+else:
+ if verbose:
+ logger.info("not updated dt")
+
+
+# calculate the number of time steps to reach steady state
+t_end = np.sqrt(kgrid.x_size**2 + kgrid.y_size**2) / c0_min
+
+# create the time array using an integer number of points per period
+Nt = round(t_end / dt)
+
+# make time array
+kgrid.setTime(Nt, dt)
+
+# calculate the actual CFL after adjusting for dt
+cfl_actual = 1.0 / (dt * freq)
+
+if verbose:
+ logger.info('PPW = ' + str(ppw))
+ logger.info('CFL = ' + str(cfl_actual))
+ logger.info('PPP = ' + str(ppp))
+
+
+# =========================================================================
+# DEFINE THE SOURCE PARAMETERS
+# =========================================================================
+
+if verbose:
+ logger.info("kSource")
+
+# create empty kWaveArray this specfies the transducer properties
+karray = kWaveArray(bli_tolerance=0.01,
+ upsampling_rate=16,
+ single_precision=True)
+
+if (transducer == 'sc1'):
+
+ # set bowl position and orientation
+ bowl_pos = [kgrid.x_vec[bowl_coords[0]].item(),
+ kgrid.y_vec[bowl_coords[1]].item(),
+ kgrid.z_vec[bowl_coords[2]].item()]
+
+ focus_pos = [kgrid.x_vec[focus_coords[0]].item(),
+ kgrid.y_vec[focus_coords[1]].item(),
+ kgrid.z_vec[focus_coords[2]].item()]
+
+ # add bowl shaped element
+ karray.add_bowl_element(bowl_pos, source_roc, diameters, focus_pos)
+
+elif (transducer == 'sc2'):
+
+ # set disc position
+ position = [kgrid.x_vec[disc_coords[0]].item(),
+ kgrid.y_vec[disc_coords[1]].item(),
+ kgrid.z_vec[disc_coords[2]].item()]
+
+ # arbitrary position
+ focus_pos = [kgrid.x_vec[focus_coords[0]].item(),
+ kgrid.y_vec[focus_coords[1]].item(),
+ kgrid.z_vec[focus_coords[2]].item()]
+
+ # add disc-shaped planar element
+ karray.add_disc_element(position, diameter, focus_pos)
+
+# create time varying source
+source_sig = create_cw_signals(np.squeeze(kgrid.t_array),
+ freq,
+ source_amp,
+ source_phase)
+
+# 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)
+
+
+# =========================================================================
+# DEFINE THE SENSOR PARAMETERS
+# =========================================================================
+
+if verbose:
+ logger.info("kSensor")
+
+sensor = kSensor()
+
+# set sensor mask: the mask says at which points data should be recorded
+sensor.mask = np.ones((Nx, Ny, Nz), dtype=bool)
+
+# set the record type: record the pressure waveform
+sensor.record = ['p']
+
+# record 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
+# =========================================================================
+
+DATA_CAST = 'single'
+DATA_PATH = './'
+
+input_filename = tag + '_' + transducer + '_' + res + '_input.h5'
+output_filename = tag + '_' + transducer + '_' + res + '_output.h5'
+
+# set input options
+if verbose:
+ logger.info("simulation_options")
+
+# options for writing to file, but not doing simulations
+simulation_options = SimulationOptions(
+ data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename,
+ output_filename=output_filename,
+ save_to_disk_exit=False,
+ data_path=DATA_PATH,
+ pml_inside=False)
+
+if verbose:
+ logger.info("execution_options")
+
+execution_options = SimulationExecutionOptions(
+ is_gpu_simulation=True,
+ delete_data=False,
+ verbose_level=2)
+
+
+
+# =========================================================================
+# RUN THE SIMULATION
+# =========================================================================
+
+if verbose:
+ logger.info("kspaceFirstOrder3DG")
+
+sensor_data = kspaceFirstOrder3DG(
+ medium=medium,
+ kgrid=kgrid,
+ source=source,
+ sensor=sensor,
+ simulation_options=simulation_options,
+ execution_options=execution_options)
+
+
+# =========================================================================
+# POST-PROCESS
+# =========================================================================
+
+if verbose:
+ logger.info("post processing")
+
+# sampling frequency
+fs = 1.0 / kgrid.dt
+
+if verbose:
+ logger.info("extract_amp_phase")
+
+# get Fourier coefficients
+amp, _, _ = extract_amp_phase(sensor_data['p'].T, fs, freq, dim=1,
+ fft_padding=1, window='Rectangular')
+
+# reshape data: matlab uses Fortran ordering
+p = np.reshape(amp, (Nx, Ny, Nz), order='F')
+
+x = np.linspace(-Nx // 2, Nx // 2 - 1, Nx)
+y = np.linspace(-Ny // 2, Ny // 2 - 1, Ny)
+z = np.linspace(-Nz // 2, Nz // 2 - 1, Nz)
+x, y, z = np.meshgrid(x, y, z, indexing='ij')
+
+pmax = np.nanmax(p)
+max_loc = np.unravel_index(np.nanargmax(p), p.shape, order='C')
+
+p_water = np.empty_like(p)
+p_water.fill(np.nan)
+p_water[water_mask] = p[water_mask]
+pmax_water = np.nanmax(p_water)
+max_loc_water = np.unravel_index(np.nanargmax(p_water), p.shape, order='C')
+
+p_skull = np.empty_like(p)
+p_skull.fill(np.nan)
+p_skull[skull_mask] = p[skull_mask]
+pmax_skull = np.nanmax(p_skull)
+max_loc_skull = np.unravel_index(np.nanargmax(p_skull), p.shape, order='C')
+
+p_brain = np.empty_like(p)
+p_brain.fill(np.nan)
+p_brain[brain_mask] = p[brain_mask]
+pmax_brain = np.nanmax(p_brain)
+max_loc_brain = np.unravel_index(np.nanargmax(p_brain), p.shape, order='C')
+
+# domain axes
+x_vec = np.linspace(kgrid.x_vec[0].item(), kgrid.x_vec[-1].item(), kgrid.Nx)
+y_vec = np.linspace(kgrid.y_vec[0].item(), kgrid.y_vec[-1].item(), kgrid.Ny)
+z_vec = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+
+# colours
+cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
+
+# brain axes
+# x
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[0]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+x_x = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_brain[2]].item()]
+y_x = [kgrid.x_vec[indy].item(), kgrid.x_vec[max_loc_brain[0]].item()]
+coefficients_x = np.polyfit(x_x, y_x, 1)
+polynomial_x = np.poly1d(coefficients_x)
+axis = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+beam_axis_x = polynomial_x(z_vec)
+# y
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[1]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+x_y = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_brain[2]].item()]
+y_y = [kgrid.y_vec[indy].item(), kgrid.y_vec[max_loc_brain[1]].item()]
+coefficients_y = np.polyfit(x_y, y_y, 1)
+polynomial_y = np.poly1d(coefficients_y)
+beam_axis_y = polynomial_y(z_vec)
+# beam axis
+beam_axis = np.vstack((beam_axis_x, beam_axis_y, z_vec)).T
+# interpolate for pressure on brain axis
+beam_pressure_brain = interpn((x_vec, y_vec, z_vec) , p, beam_axis,
+ method='linear', bounds_error=False, fill_value=np.nan)
+
+# skull axes
+# x
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[0]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[0]
+x_x = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_skull[2]].item()]
+y_x = [kgrid.x_vec[indy].item(), kgrid.x_vec[max_loc_skull[0]].item()]
+coefficients_x = np.polyfit(x_x, y_x, 1)
+polynomial_x = np.poly1d(coefficients_x)
+axis = np.linspace(kgrid.z_vec[0].item(), kgrid.z_vec[-1].item(), kgrid.Nz)
+beam_axis_x = polynomial_x(z_vec)
+# y
+if (transducer == 'sc1'):
+ indx = bowl_coords[2]
+ indy = bowl_coords[1]
+elif (transducer == 'sc2'):
+ indx = disc_coords[2]
+ indy = disc_coords[1]
+x_y = [kgrid.z_vec[indx].item(), kgrid.z_vec[max_loc_skull[2]].item()]
+y_y = [kgrid.y_vec[indy].item(), kgrid.y_vec[max_loc_skull[1]].item()]
+coefficients_y = np.polyfit(x_y, y_y, 1)
+polynomial_y = np.poly1d(coefficients_y)
+beam_axis_y = polynomial_y(z_vec)
+
+# beam axis
+beam_axis = np.vstack((beam_axis_x, beam_axis_y, z_vec)).T
+
+# interpolate for pressure
+beam_pressure_skull = interpn((x_vec, y_vec, z_vec) , p, beam_axis,
+ method='linear', bounds_error=False, fill_value=np.nan)
+
+
+
+# plot pressure on through centre lines
+fig1, ax1 = plt.subplots()
+ax1.plot(p[(Nx-1)//2, (Nx-1)//2, :] / 1e6, label='geometric')
+ax1.plot(beam_pressure_brain / 1e6, label='focal')
+ax1.plot(beam_pressure_skull / 1e6, label='skull')
+ax1.hlines(pmax_brain / 1e6, 0, len(z_vec), color=cycle[1], linestyle='dashed', lw=0.5)
+ax1.hlines(pmax_skull / 1e6, 0, len(z_vec), color=cycle[2], linestyle='dashed', lw=0.5)
+ax1.set(xlabel='Axial Position [mm]',
+ ylabel='Pressure [MPa]',
+ title='Centreline Pressures')
+ax1.legend()
+ax1.grid(True)
+
+
+
+def get_edges(mask, fill_with_nan=True):
+ """returns the mask as a float array and Np.NaN"""
+ edges = find_boundaries(mask, mode='thin').astype(np.float32)
+ if fill_with_nan:
+ edges[edges == 0] = np.nan
+ return edges
+
+# contouring block
+
+edges_x = get_edges(np.transpose(skull_mask[max_loc_brain[0], :, :]).astype(int), fill_with_nan=False)
+edges_y = get_edges(np.transpose(skull_mask[:, max_loc_brain[1], :]).astype(int), fill_with_nan=False)
+edges_z = get_edges(np.transpose(skull_mask[:, :, max_loc_brain[2]]).astype(int), fill_with_nan=False)
+
+contour_x, num_x = measure.label(edges_x, background=0, return_num=True, connectivity=2)
+contour_y, num_y = measure.label(edges_y, background=0, return_num=True, connectivity=2)
+contour_z, num_z = measure.label(edges_z, background=0, return_num=True, connectivity=2)
+
+if verbose:
+ msg = "size of contours:" + str(np.shape(contour_x)) + ", " + str(np.shape(contour_y)) + ", " + str(np.shape(contour_z)) + "."
+ logger.info(msg)
+ msg = "number of contours: (" + str(num_x) + ", " + str(num_y) + ", " + str(num_z) + ")."
+ logger.info(msg)
+
+jmax = 0
+jmin = Ny
+i_inner = None
+i_outer = None
+# for a number of contours
+for i in range(num_x):
+ idx = int(np.shape(contour_x)[1] // 2)
+ j = np.argmax(np.where(contour_x[:, idx]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i + 1
+ k = np.argmin(np.where(contour_x[:, idx]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i + 1
+contours_x_inner = measure.find_contours(np.where(contour_x==i_inner, 1, 0))
+if not contours_x_inner:
+ logger.warning("size of contours_x_inner is zero")
+contours_x_outer = measure.find_contours(np.where(contour_x==i_outer, 1, 0))
+if not contours_x_outer:
+ logger.warning("size of contours_x_outer is zero")
+inner_index_x = float(Ny)
+outer_index_x = float(0)
+for i in range(len(contours_x_inner)):
+ x_min = np.min(contours_x_inner[i][:, 1])
+ if (x_min < inner_index_x):
+ inner_index_x = i
+for i in range( len(contours_x_outer) ):
+ x_max = np.max(contours_x_outer[i][:, 1])
+ if (x_max > outer_index_x):
+ outer_index_x = i
+
+jmax = 0
+jmin = Nx
+i_inner = None
+i_outer = None
+for i in range(num_y):
+ idy: int = int(np.shape(contour_y)[1] // 2)
+ j = np.argmax(np.where(contour_y[:, idy]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i + 1
+ k = np.argmin(np.where(contour_y[:, idy]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i + 1
+contours_y_inner = measure.find_contours(np.where(contour_y==i_inner, 1, 0))
+if not contours_y_inner:
+ logger.warning("size of contours_y_inner is zero")
+contours_y_outer = measure.find_contours(np.where(contour_y==i_outer, 1, 0))
+if not contours_y_outer:
+ logger.warning("size of contours_y_outer is zero")
+inner_index_y = float(Nx)
+outer_index_y = float(0)
+for i in range( len(contours_y_inner) ):
+ y_min = np.min(contours_y_inner[i][:, 1])
+ if (y_min < inner_index_y):
+ inner_index_y = i
+for i in range( len(contours_y_outer) ):
+ y_max = np.max(contours_y_outer[i][:, 1])
+ if (y_max > outer_index_y):
+ outer_index_y = i
+
+jmax = 0
+jmin = Ny
+i_inner = None
+i_outer = None
+for i in range(num_z):
+ idz: int = int(np.shape(contour_z)[1] // 2)
+ j = np.argmax(np.where(contour_z[:, idz]==(i+1), 1, 0))
+ if (j > jmax):
+ jmax = j
+ i_outer = i+1
+ k = np.argmin(np.where(contour_z[:, idz]==(i+1), 0, 1))
+ if (k < jmin):
+ jmin = k
+ i_inner = i+1
+
+contours_z_inner = measure.find_contours(np.where(contour_z==i_inner, 1, 0))
+if not contours_z_inner:
+ logger.warning("size of contours_z_inner is zero")
+else:
+ inner_index_z = float(Nx)
+ for i in range( len(contours_z_inner) ):
+ z_min = np.min(contours_z_inner[i][:, 1])
+ if (z_min < inner_index_z):
+ inner_index_z = i
+
+contours_z_outer = measure.find_contours(np.where(contour_z==i_outer, 1, 0))
+if not contours_z_outer:
+ logger.warning("size of contours_z_outer is zero")
+else:
+ outer_index_z = float(0)
+ for i in range( len(contours_z_outer) ):
+ z_max = np.max(contours_z_outer[i][:, 1])
+ if (z_max > outer_index_z):
+ outer_index_z = i
+
+# end of contouring block
+
+edges_x = get_edges(np.transpose(skull_mask[max_loc_brain[0], :, :]).astype(int))
+edges_y = get_edges(np.transpose(skull_mask[:, max_loc_brain[1], :]).astype(int))
+edges_z = get_edges(np.transpose(skull_mask[:, :, max_loc_brain[2]]).astype(int), fill_with_nan=True)
+
+# plot the pressure field at mid point along z axis
+fig2, ax2 = plt.subplots()
+im2 = ax2.imshow(p[:, :, max_loc_brain[2]] / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='lower',
+ cmap='viridis')
+
+if not contours_z_inner:
+ ax2.imshow(edges_z, aspect='auto', interpolation='none',
+ cmap='Greys', origin='upper')
+else:
+ ax2.plot(contours_z_inner[inner_index_z][:, 1],
+ contours_z_inner[inner_index_z][:, 0], 'w', linewidth=0.5)
+if not contours_z_outer:
+ pass
+else:
+ ax2.plot(contours_z_outer[outer_index_z][:, 1],
+ contours_z_outer[outer_index_z][:, 0], 'w', linewidth=0.5)
+
+ax2.set(xlabel=r'$x$ [mm]',
+ ylabel=r'$y$ [mm]',
+ title='Pressure Field')
+ax2.grid(False)
+divider2 = make_axes_locatable(ax2)
+cax2 = divider2.append_axes("right", size="5%", pad=0.05)
+cbar_2 = fig2.colorbar(im2, cax=cax2)
+cbar_2.ax.set_title('[MPa]', fontsize='small')
+
+pwater_max_x = np.nanmax(p_water[max_loc_brain[0], :, :].flatten())
+pskull_max_x = np.nanmax(p_skull[max_loc_brain[0], :, :].flatten())
+pbrain_max_x = np.nanmax(p_brain[max_loc_brain[0], :, :].flatten())
+
+pwater_max_y = np.nanmax(p_water[:, max_loc_brain[1], :].flatten())
+pskull_max_y = np.nanmax(p_skull[:, max_loc_brain[1], :].flatten())
+pbrain_max_y = np.nanmax(p_brain[:, max_loc_brain[1], :].flatten())
+
+fig3, (ax3a, ax3b) = plt.subplots(1,2)
+im3a_water = ax3a.imshow(p_water[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='cool')
+im3a_skull = ax3a.imshow(p_skull[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='turbo')
+im3a_brain = ax3a.imshow(p_brain[max_loc_brain[0], :, :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax3a.plot(contours_x_inner[inner_index_x][:, 1],
+ contours_x_inner[inner_index_x][:, 0], 'k', linewidth=0.5)
+ax3a.plot(contours_x_outer[outer_index_x][:, 1],
+ contours_x_outer[outer_index_x][:, 0], 'k', linewidth=0.5)
+
+ax3a.grid(False)
+ax3a.axes.get_yaxis().set_visible(False)
+ax3a.axes.get_xaxis().set_visible(False)
+divider3a = make_axes_locatable(ax3a)
+cax3a = divider3a.append_axes("right", size="5%", pad=0.05)
+cbar_3a = fig3.colorbar(im3a_brain, cax=cax3a)
+cbar_3a.ax.set_title('[kPa]', fontsize='small')
+ax3b.imshow(p_water[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='cool')
+ax3b.imshow(p_skull[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='turbo')
+im3b_brain = ax3b.imshow(p_brain[:, max_loc_brain[1], :].T,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+ax3b.grid(False)
+ax3b.axes.get_yaxis().set_visible(False)
+ax3b.axes.get_xaxis().set_visible(False)
+divider3b = make_axes_locatable(ax3b)
+cax3b = divider3b.append_axes("right", size="5%", pad=0.05)
+cbar_3b = fig3.colorbar(im3b_brain, cax=cax3b)
+cbar_3b.ax.set_title('[Pa]', fontdict={'fontsize':8})
+
+
+fig4, ax4 = plt.subplots()
+if not contours_z_inner:
+ pass
+else:
+ ax4.plot(contours_z_inner[inner_index_z][:, 1],
+ contours_z_inner[inner_index_z][:, 0], 'w', linewidth=0.5)
+if not contours_z_outer:
+ pass
+else:
+ ax4.plot(contours_z_outer[outer_index_z][:, 1],
+ contours_z_outer[outer_index_z][:, 0], 'w', linewidth=0.5)
+
+
+fig5, (ax5a, ax5b) = plt.subplots(1,2)
+im5a = ax5a.imshow(p[max_loc_brain[0], :, :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+im5a_boundary = ax5a.imshow(edges_x, aspect='auto', interpolation='none',
+ cmap='Greys', origin='upper', alpha=0.75)
+ax5a.grid(False)
+ax5a.axes.get_yaxis().set_visible(False)
+ax5a.axes.get_xaxis().set_visible(False)
+divider5a = make_axes_locatable(ax5a)
+cax5a = divider5a.append_axes("right", size="5%", pad=0.05)
+cbar_5a = fig5.colorbar(im5a, cax=cax5a)
+cbar_5a.ax.set_title('[MPa]', fontsize='small')
+im5b = ax5b.imshow(p[:, max_loc_brain[1], :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+im5b_boundary = ax5b.imshow(edges_y, aspect='auto', interpolation='none',
+ cmap='Greys',origin='upper', alpha=0.75)
+ax5b.grid(False)
+ax5b.axes.get_yaxis().set_visible(False)
+ax5b.axes.get_xaxis().set_visible(False)
+divider5b = make_axes_locatable(ax5b)
+cax5b = divider5b.append_axes("right", size="5%", pad=0.05)
+cbar_5b = fig5.colorbar(im5b, cax=cax5b)
+cbar_5b.ax.set_title('[MPa]', fontsize='small')
+
+all_contours_x = []
+for i in range(num_x):
+ all_contours_x.append(measure.find_contours(np.where(contour_x==(i+1), 1, 0)))
+
+all_contours_y = []
+for i in range(num_y):
+ all_contours_y.append(measure.find_contours(np.where(contour_y==(i+1), 1, 0)))
+
+fig6, (ax6a, ax6b) = plt.subplots(1,2)
+im6a = ax6a.imshow(p[max_loc_brain[0], :, :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+for contour in all_contours_x:
+ # logger.info(contour dir(contour))
+ for i in range( len(contour) ):
+ ax6a.plot(contour[i][:, 1], contour[i][:, 0], 'w', linewidth=0.5)
+
+ax6a.grid(False)
+ax6a.axes.get_yaxis().set_visible(False)
+ax6a.axes.get_xaxis().set_visible(False)
+divider6a = make_axes_locatable(ax5a)
+cax6a = divider6a.append_axes("right", size="5%", pad=0.05)
+cbar_6a = fig6.colorbar(im6a, cax=cax6a)
+cbar_6a.ax.set_title('[MPa]', fontsize='small')
+im6b = ax6b.imshow(p[:, max_loc_brain[1], :].T / 1e6,
+ vmin=0, vmax=pmax / 1e6,
+ aspect='auto',
+ interpolation='none',
+ origin='upper',
+ cmap='viridis')
+
+custom_cycler = cycler(ls=['-', '--', ':', '-.'])
+
+ax6b.set_prop_cycle(custom_cycler)
+
+for idx, contour in enumerate(all_contours_y):
+ for i in range( len(contour) ):
+ ax6b.plot(contour[i][:, 1], contour[i][:, 0], c=cycle[idx],
+ linewidth=0.5, label=str(idx))
+ax6b.legend()
+ax6b.grid(False)
+ax6b.axes.get_yaxis().set_visible(False)
+ax6b.axes.get_xaxis().set_visible(False)
+divider6b = make_axes_locatable(ax6b)
+cax6b = divider6b.append_axes("right", size="5%", pad=0.05)
+cbar_6b = fig6.colorbar(im6b, cax=cax6b)
+cbar_6b.ax.set_title('[MPa]', fontsize='small')
+
+# plt.show()
+
+plotter = pv.Plotter()
+
+pmax = np.nanmax(p)
+pmin = np.nanmin(p)
+
+grid = pv.ImageData()
+grid.dimensions = np.array(p.shape) + 1
+grid.spacing = (1, 1, 1)
+grid.cell_data['pressure'] = np.ravel(p, order="F")
+
+xslice_depth = max_loc_brain[0]
+yslice_depth = max_loc_brain[1]
+zslice_depth = max_loc_brain[2]
+
+
+
+slice_x_focus = grid.slice(normal='x', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+slice_y_focus = grid.slice(normal='y', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+slice_z_focus = grid.slice(normal='z', origin=[xslice_depth, yslice_depth, zslice_depth],
+ generate_triangles=False, contour=False, progress_bar=False)
+
+# slice_array = slice_z_focus.cell_data['pressure'].reshape(grid.dimensions[0]-1, grid.dimensions[1]-1)
+
+slice_z_tx = grid.slice(normal='-z', origin=disc_coords,
+ generate_triangles=False, contour=False, progress_bar=False)
+
+# slice_array = slice_z_tx.cell_data['pressure'].reshape(grid.dimensions[0]-1, grid.dimensions[1]-1)
+
+slice_z_rx = grid.slice(normal='z', origin=[(Nx-1) // 2, (Ny - 1) // 2, Nz-1],
+ generate_triangles=False, contour=False, progress_bar=False)
+
+slice_array = slice_z_rx.cell_data['pressure'].reshape(grid.dimensions[0]-1, grid.dimensions[1]-1)
+
+# now get points on skull surfaces
+verts, faces, normals, _ = measure.marching_cubes(skull_mask, 0)
+
+vfaces = np.column_stack((np.ones(len(faces),) * 3, faces)).astype(int)
+
+x = np.arange(p.shape[0]) # X-coordinates
+y = np.arange(p.shape[1]) # Y-coordinates
+z = np.arange(p.shape[2]) # Z-coordinates
+
+# set up a interpolator
+interpolator = RegularGridInterpolator((x, y, z), p)
+# get the pressure values on the vertices
+interpolated_values = interpolator(verts)
+
+# set up mesh for skull surface
+mesh = pv.PolyData(verts, vfaces)
+mesh['Normals'] = normals
+
+# Assign interpolated data to mesh
+mesh.point_data['abs pressure'] = interpolated_values
+# clip data
+mesh.point_data['abs pressure'] = np.where(mesh.point_data['abs pressure'] > pmax_brain, pmax_brain, mesh.point_data['abs pressure'] )
+
+if verbose:
+ msg = 'focus in brain: ' + str(max_loc_brain) + ', mid point: ' + str(disc_coords) + ' last plane: ' + str(np.unravel_index(np.argmax(slice_array), slice_array.shape))
+ logger.info(msg)
+
+# Choose a colormap
+plotter.add_mesh(mesh, scalars='abs pressure', opacity=0.25, show_edges=False, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=True)
+plotter.add_mesh(slice_x_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_y_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_focus, opacity=0.95, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_tx, opacity=0.75, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.add_mesh(slice_z_rx, opacity=0.75, cmap='viridis', clim=[pmin, pmax_brain], show_scalar_bar=False)
+plotter.show_axes()
+plotter.show_bounds()
+
+plotter.show()
diff --git a/examples/benchmarks/8/runner.log b/examples/benchmarks/8/runner.log
new file mode 100644
index 000000000..b7dbe29ee
--- /dev/null
+++ b/examples/benchmarks/8/runner.log
@@ -0,0 +1,615 @@
+2025-01-06 14:27:13,067 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-06 14:27:13,070 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-06 14:27:14,340 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-06 14:27:14,341 | __main__ | INFO | not updated dt
+2025-01-06 14:27:14,342 | __main__ | INFO | PPW = 3.0
+2025-01-06 14:27:14,343 | __main__ | INFO | CFL = 20.0
+2025-01-06 14:27:14,343 | __main__ | INFO | PPP = 20
+2025-01-06 14:27:14,344 | __main__ | INFO | kSource
+2025-01-06 14:30:10,247 | __main__ | INFO | kSensor
+2025-01-06 14:30:10,251 | __main__ | INFO | simulation_options
+2025-01-06 14:30:10,252 | __main__ | INFO | execution_options
+2025-01-06 14:30:10,254 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-06 15:23:52,782 | __main__ | INFO | post processing
+2025-01-06 15:23:52,788 | __main__ | INFO | extract_amp_phase
+2025-01-06 15:25:45,083 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-06 15:25:45,089 | __main__ | INFO | number of contours: (2, 4, 2).
+2025-01-06 15:49:26,030 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-06 15:49:26,037 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-06 15:49:27,810 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-06 15:49:27,811 | __main__ | INFO | not updated dt
+2025-01-06 15:49:27,813 | __main__ | INFO | PPW = 3.0
+2025-01-06 15:49:27,814 | __main__ | INFO | CFL = 20.0
+2025-01-06 15:49:27,815 | __main__ | INFO | PPP = 20
+2025-01-06 15:49:27,816 | __main__ | INFO | kSource
+2025-01-06 15:52:17,839 | __main__ | INFO | kSensor
+2025-01-06 15:52:17,841 | __main__ | INFO | simulation_options
+2025-01-06 15:52:17,843 | __main__ | INFO | execution_options
+2025-01-06 15:52:36,606 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-06 16:04:07,009 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-06 16:04:07,012 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-06 16:04:08,013 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-06 16:04:08,014 | __main__ | INFO | not updated dt
+2025-01-06 16:04:08,014 | __main__ | INFO | PPW = 3.0
+2025-01-06 16:04:08,015 | __main__ | INFO | CFL = 20.0
+2025-01-06 16:04:08,015 | __main__ | INFO | PPP = 20
+2025-01-06 16:04:08,016 | __main__ | INFO | kSource
+2025-01-06 16:05:40,729 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-06 16:05:40,736 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-06 16:05:42,591 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-06 16:05:42,592 | __main__ | INFO | not updated dt
+2025-01-06 16:05:42,594 | __main__ | INFO | PPW = 3.0
+2025-01-06 16:05:42,595 | __main__ | INFO | CFL = 20.0
+2025-01-06 16:05:42,596 | __main__ | INFO | PPP = 20
+2025-01-06 16:05:42,598 | __main__ | INFO | kSource
+2025-01-06 16:08:26,878 | __main__ | INFO | kSensor
+2025-01-06 16:08:26,880 | __main__ | INFO | simulation_options
+2025-01-06 16:08:26,882 | __main__ | INFO | execution_options
+2025-01-06 16:08:46,715 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-06 17:46:23,886 | __main__ | INFO | post processing
+2025-01-06 17:46:23,890 | __main__ | INFO | extract_amp_phase
+2025-01-06 17:47:48,382 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-06 17:47:48,394 | __main__ | INFO | number of contours: (2, 4, 2).
+2025-01-07 12:55:32,670 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-07 12:55:32,675 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-07 12:55:33,901 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-07 12:55:33,902 | __main__ | INFO | not updated dt
+2025-01-07 12:55:33,903 | __main__ | INFO | PPW = 3.0
+2025-01-07 12:55:33,904 | __main__ | INFO | CFL = 20.0
+2025-01-07 12:55:33,904 | __main__ | INFO | PPP = 20
+2025-01-07 12:55:33,905 | __main__ | INFO | kSource
+2025-01-07 12:58:04,155 | __main__ | INFO | kSensor
+2025-01-07 12:58:04,157 | __main__ | INFO | simulation_options
+2025-01-07 12:58:04,159 | __main__ | INFO | execution_options
+2025-01-07 12:58:13,708 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-07 14:59:43,278 | __main__ | INFO | post processing
+2025-01-07 14:59:43,284 | __main__ | INFO | extract_amp_phase
+2025-01-07 15:01:55,859 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-07 15:01:55,870 | __main__ | INFO | number of contours: (2, 4, 2).
+2025-01-07 15:20:12,837 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-07 15:20:12,850 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-07 15:20:14,595 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-07 15:20:14,596 | __main__ | INFO | not updated dt
+2025-01-07 15:20:14,597 | __main__ | INFO | PPW = 3.0
+2025-01-07 15:20:14,598 | __main__ | INFO | CFL = 20.0
+2025-01-07 15:20:14,599 | __main__ | INFO | PPP = 20
+2025-01-07 15:20:14,600 | __main__ | INFO | kSource
+2025-01-07 15:22:32,616 | __main__ | INFO | kSensor
+2025-01-07 15:22:32,619 | __main__ | INFO | simulation_options
+2025-01-07 15:22:32,620 | __main__ | INFO | execution_options
+2025-01-07 15:22:41,828 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-07 15:22:41,829 | __main__ | INFO | post processing
+2025-01-07 15:22:41,831 | __main__ | INFO | extract_amp_phase
+2025-01-07 15:22:43,315 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-07 15:22:43,316 | __main__ | INFO | number of contours: (2, 1, 2).
+2025-01-07 15:27:46,240 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-07 15:27:46,253 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-07 15:27:49,446 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-07 15:27:49,449 | __main__ | INFO | not updated dt
+2025-01-07 15:27:49,455 | __main__ | INFO | PPW = 3.0
+2025-01-07 15:27:49,457 | __main__ | INFO | CFL = 20.0
+2025-01-07 15:27:49,464 | __main__ | INFO | PPP = 20
+2025-01-07 15:27:49,467 | __main__ | INFO | kSource
+2025-01-07 15:30:44,260 | __main__ | INFO | kSensor
+2025-01-07 15:30:44,268 | __main__ | INFO | simulation_options
+2025-01-07 15:30:44,273 | __main__ | INFO | execution_options
+2025-01-07 15:31:48,010 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-07 15:31:48,010 | __main__ | INFO | post processing
+2025-01-07 15:31:48,012 | __main__ | INFO | extract_amp_phase
+2025-01-07 15:31:49,468 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-07 15:31:49,468 | __main__ | INFO | number of contours: (2, 1, 2).
+2025-01-07 15:32:31,343 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-07 15:32:31,350 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-07 15:32:33,233 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-07 15:32:33,233 | __main__ | INFO | not updated dt
+2025-01-07 15:32:33,235 | __main__ | INFO | PPW = 3.0
+2025-01-07 15:32:33,237 | __main__ | INFO | CFL = 20.0
+2025-01-07 15:32:33,237 | __main__ | INFO | PPP = 20
+2025-01-07 15:32:33,237 | __main__ | INFO | kSource
+2025-01-07 15:35:25,349 | __main__ | INFO | kSensor
+2025-01-07 15:35:25,352 | __main__ | INFO | simulation_options
+2025-01-07 15:35:25,352 | __main__ | INFO | execution_options
+2025-01-07 15:36:24,374 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-07 15:36:24,375 | __main__ | INFO | post processing
+2025-01-07 15:36:24,375 | __main__ | INFO | extract_amp_phase
+2025-01-07 15:36:26,128 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-07 15:36:26,129 | __main__ | INFO | number of contours: (2, 1, 2).
+2025-01-07 15:39:50,970 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-07 15:39:50,975 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-07 15:39:52,845 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-07 15:39:52,846 | __main__ | INFO | not updated dt
+2025-01-07 15:39:52,847 | __main__ | INFO | PPW = 3.0
+2025-01-07 15:39:52,847 | __main__ | INFO | CFL = 20.0
+2025-01-07 15:39:52,848 | __main__ | INFO | PPP = 20
+2025-01-07 15:39:52,849 | __main__ | INFO | kSource
+2025-01-07 15:42:16,296 | __main__ | INFO | kSensor
+2025-01-07 15:42:16,306 | __main__ | INFO | simulation_options
+2025-01-07 15:42:16,309 | __main__ | INFO | execution_options
+2025-01-07 15:42:16,314 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-07 15:42:16,317 | __main__ | INFO | post processing
+2025-01-07 15:42:16,319 | __main__ | INFO | extract_amp_phase
+2025-01-07 15:42:17,885 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-07 15:42:17,888 | __main__ | INFO | number of contours: (2, 1, 2).
+2025-01-07 15:45:42,048 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-07 15:45:42,053 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-07 15:45:43,809 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-07 15:45:43,810 | __main__ | INFO | not updated dt
+2025-01-07 15:45:43,811 | __main__ | INFO | PPW = 3.0
+2025-01-07 15:45:43,812 | __main__ | INFO | CFL = 20.0
+2025-01-07 15:45:43,813 | __main__ | INFO | PPP = 20
+2025-01-07 15:45:43,813 | __main__ | INFO | kSource
+2025-01-07 15:48:38,699 | __main__ | INFO | kSensor
+2025-01-07 15:48:38,701 | __main__ | INFO | simulation_options
+2025-01-07 15:48:38,703 | __main__ | INFO | execution_options
+2025-01-07 15:48:38,703 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-07 15:48:38,705 | __main__ | INFO | post processing
+2025-01-07 15:48:38,705 | __main__ | INFO | extract_amp_phase
+2025-01-07 15:48:40,045 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-07 15:48:40,046 | __main__ | INFO | number of contours: (2, 1, 2).
+2025-01-07 15:52:59,642 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-07 15:52:59,646 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-07 15:53:02,044 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-07 15:53:02,045 | __main__ | INFO | not updated dt
+2025-01-07 15:53:02,047 | __main__ | INFO | PPW = 3.0
+2025-01-07 15:53:02,049 | __main__ | INFO | CFL = 20.0
+2025-01-07 15:53:02,050 | __main__ | INFO | PPP = 20
+2025-01-07 15:53:02,051 | __main__ | INFO | kSource
+2025-01-07 15:55:31,730 | __main__ | INFO | kSensor
+2025-01-07 15:55:31,740 | __main__ | INFO | simulation_options
+2025-01-07 15:55:31,745 | __main__ | INFO | execution_options
+2025-01-07 15:55:31,749 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-07 16:58:36,098 | __main__ | INFO | post processing
+2025-01-07 16:58:36,103 | __main__ | INFO | extract_amp_phase
+2025-01-07 17:00:26,668 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-07 17:00:26,678 | __main__ | INFO | number of contours: (2, 1, 2).
+2025-01-07 17:02:42,638 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-07 17:02:42,648 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-07 17:02:44,349 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-07 17:02:44,350 | __main__ | INFO | not updated dt
+2025-01-07 17:02:44,350 | __main__ | INFO | PPW = 3.0
+2025-01-07 17:02:44,350 | __main__ | INFO | CFL = 20.0
+2025-01-07 17:02:44,351 | __main__ | INFO | PPP = 20
+2025-01-07 17:02:44,351 | __main__ | INFO | kSource
+2025-01-07 17:04:55,084 | __main__ | INFO | kSensor
+2025-01-07 17:04:55,090 | __main__ | INFO | simulation_options
+2025-01-07 17:04:55,090 | __main__ | INFO | execution_options
+2025-01-07 17:04:55,092 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-07 18:03:30,766 | __main__ | INFO | post processing
+2025-01-07 18:03:30,770 | __main__ | INFO | extract_amp_phase
+2025-01-07 18:05:17,635 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-07 18:05:17,642 | __main__ | INFO | number of contours: (2, 4, 2).
+2025-01-08 08:53:02,417 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 08:53:02,422 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 08:53:03,488 | __main__ | INFO | dt_stability_limit=1.7147615847964107e-07, dt=1e-07
+2025-01-08 08:53:03,488 | __main__ | INFO | not updated dt
+2025-01-08 08:53:03,489 | __main__ | INFO | PPW = 3.0
+2025-01-08 08:53:03,489 | __main__ | INFO | CFL = 20.0
+2025-01-08 08:53:03,490 | __main__ | INFO | PPP = 20
+2025-01-08 08:53:03,490 | __main__ | INFO | kSource
+2025-01-08 08:55:15,732 | __main__ | INFO | kSensor
+2025-01-08 08:55:15,736 | __main__ | INFO | simulation_options
+2025-01-08 08:55:15,737 | __main__ | INFO | execution_options
+2025-01-08 08:55:15,738 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 10:34:20,673 | __main__ | INFO | post processing
+2025-01-08 10:34:20,676 | __main__ | INFO | extract_amp_phase
+2025-01-08 10:36:00,093 | __main__ | INFO | size of contours:(226, 171), (226, 191), (171, 191).
+2025-01-08 10:36:00,103 | __main__ | INFO | number of contours: (2, 4, 2).
+2025-01-08 11:51:41,496 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 11:51:41,507 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 11:57:18,237 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 11:57:18,240 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 11:57:18,514 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 11:57:18,516 | __main__ | INFO | transducer is planar
+2025-01-08 11:57:18,856 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 11:57:18,858 | __main__ | INFO | not updated dt
+2025-01-08 11:57:18,860 | __main__ | INFO | PPW = 1.875
+2025-01-08 11:57:18,860 | __main__ | INFO | CFL = 20.0
+2025-01-08 11:57:18,861 | __main__ | INFO | PPP = 20
+2025-01-08 11:57:18,862 | __main__ | INFO | kSource
+2025-01-08 11:57:19,003 | __main__ | INFO | kSensor
+2025-01-08 11:57:19,004 | __main__ | INFO | simulation_options
+2025-01-08 11:57:19,006 | __main__ | INFO | execution_options
+2025-01-08 11:57:19,008 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 12:00:41,154 | __main__ | INFO | post processing
+2025-01-08 12:00:41,155 | __main__ | INFO | extract_amp_phase
+2025-01-08 12:08:36,708 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 12:08:36,717 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 12:08:37,079 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 12:08:37,081 | __main__ | INFO | transducer is planar
+2025-01-08 12:08:37,653 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 12:08:37,654 | __main__ | INFO | not updated dt
+2025-01-08 12:08:37,655 | __main__ | INFO | PPW = 1.875
+2025-01-08 12:08:37,656 | __main__ | INFO | CFL = 20.0
+2025-01-08 12:08:37,658 | __main__ | INFO | PPP = 20
+2025-01-08 12:08:37,658 | __main__ | INFO | kSource
+2025-01-08 12:08:43,240 | __main__ | INFO | kSensor
+2025-01-08 12:08:43,242 | __main__ | INFO | simulation_options
+2025-01-08 12:08:43,245 | __main__ | INFO | execution_options
+2025-01-08 12:08:43,246 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 12:12:05,224 | __main__ | INFO | post processing
+2025-01-08 12:12:05,225 | __main__ | INFO | extract_amp_phase
+2025-01-08 12:12:21,403 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 12:12:21,404 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 12:28:21,522 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 12:28:21,531 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 12:28:21,928 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 12:28:21,928 | __main__ | INFO | transducer is planar
+2025-01-08 12:28:22,375 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 12:28:22,377 | __main__ | INFO | not updated dt
+2025-01-08 12:28:22,378 | __main__ | INFO | PPW = 1.875
+2025-01-08 12:28:22,378 | __main__ | INFO | CFL = 20.0
+2025-01-08 12:28:22,378 | __main__ | INFO | PPP = 20
+2025-01-08 12:28:22,380 | __main__ | INFO | kSource
+2025-01-08 12:28:25,475 | __main__ | INFO | kSensor
+2025-01-08 12:28:25,478 | __main__ | INFO | simulation_options
+2025-01-08 12:28:25,478 | __main__ | INFO | execution_options
+2025-01-08 12:28:25,481 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 12:31:41,479 | __main__ | INFO | post processing
+2025-01-08 12:31:41,479 | __main__ | INFO | extract_amp_phase
+2025-01-08 12:31:53,025 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 12:31:53,025 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 12:49:25,032 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 12:49:25,038 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 12:49:25,343 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 12:49:25,344 | __main__ | INFO | transducer is planar
+2025-01-08 12:49:25,705 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 12:49:25,706 | __main__ | INFO | not updated dt
+2025-01-08 12:49:25,706 | __main__ | INFO | PPW = 1.875
+2025-01-08 12:49:25,707 | __main__ | INFO | CFL = 20.0
+2025-01-08 12:49:25,707 | __main__ | INFO | PPP = 20
+2025-01-08 12:49:25,709 | __main__ | INFO | kSource
+2025-01-08 12:49:28,471 | __main__ | INFO | kSensor
+2025-01-08 12:49:28,473 | __main__ | INFO | simulation_options
+2025-01-08 12:49:28,475 | __main__ | INFO | execution_options
+2025-01-08 12:49:28,476 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 12:52:46,654 | __main__ | INFO | post processing
+2025-01-08 12:52:46,654 | __main__ | INFO | extract_amp_phase
+2025-01-08 12:52:58,470 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 12:52:58,471 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 13:03:19,693 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 13:03:19,696 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 13:03:19,961 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 13:03:19,961 | __main__ | INFO | transducer is planar
+2025-01-08 13:03:20,265 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 13:03:20,266 | __main__ | INFO | not updated dt
+2025-01-08 13:03:20,267 | __main__ | INFO | PPW = 1.875
+2025-01-08 13:03:20,268 | __main__ | INFO | CFL = 20.0
+2025-01-08 13:03:20,268 | __main__ | INFO | PPP = 20
+2025-01-08 13:03:20,269 | __main__ | INFO | kSource
+2025-01-08 13:03:23,513 | __main__ | INFO | kSensor
+2025-01-08 13:03:23,514 | __main__ | INFO | simulation_options
+2025-01-08 13:03:23,517 | __main__ | INFO | execution_options
+2025-01-08 13:03:23,519 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 13:06:47,924 | __main__ | INFO | post processing
+2025-01-08 13:06:47,926 | __main__ | INFO | extract_amp_phase
+2025-01-08 13:07:00,489 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 13:07:00,491 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 13:15:47,194 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 13:15:47,199 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 13:15:47,466 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 13:15:47,466 | __main__ | INFO | transducer is planar
+2025-01-08 13:15:47,782 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 13:15:47,783 | __main__ | INFO | not updated dt
+2025-01-08 13:15:47,784 | __main__ | INFO | PPW = 1.875
+2025-01-08 13:15:47,785 | __main__ | INFO | CFL = 20.0
+2025-01-08 13:15:47,785 | __main__ | INFO | PPP = 20
+2025-01-08 13:15:47,786 | __main__ | INFO | kSource
+2025-01-08 13:15:49,879 | __main__ | INFO | kSensor
+2025-01-08 13:15:49,880 | __main__ | INFO | simulation_options
+2025-01-08 13:15:49,882 | __main__ | INFO | execution_options
+2025-01-08 13:15:49,882 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 13:19:10,217 | __main__ | INFO | post processing
+2025-01-08 13:19:10,218 | __main__ | INFO | extract_amp_phase
+2025-01-08 13:19:25,314 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 13:19:25,315 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 13:21:20,345 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 13:21:20,349 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 13:21:20,727 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 13:21:20,727 | __main__ | INFO | transducer is planar
+2025-01-08 13:21:21,223 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 13:21:21,224 | __main__ | INFO | not updated dt
+2025-01-08 13:21:21,225 | __main__ | INFO | PPW = 1.875
+2025-01-08 13:21:21,225 | __main__ | INFO | CFL = 20.0
+2025-01-08 13:21:21,225 | __main__ | INFO | PPP = 20
+2025-01-08 13:21:21,227 | __main__ | INFO | kSource
+2025-01-08 13:21:24,693 | __main__ | INFO | kSensor
+2025-01-08 13:21:24,694 | __main__ | INFO | simulation_options
+2025-01-08 13:21:24,695 | __main__ | INFO | execution_options
+2025-01-08 13:21:24,697 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 13:24:48,375 | __main__ | INFO | post processing
+2025-01-08 13:24:48,376 | __main__ | INFO | extract_amp_phase
+2025-01-08 13:25:01,000 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 13:25:01,002 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 13:29:28,613 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 13:29:28,624 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 13:29:28,961 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 13:29:28,962 | __main__ | INFO | transducer is planar
+2025-01-08 13:29:29,312 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 13:29:29,313 | __main__ | INFO | not updated dt
+2025-01-08 13:29:29,314 | __main__ | INFO | PPW = 1.875
+2025-01-08 13:29:29,315 | __main__ | INFO | CFL = 20.0
+2025-01-08 13:29:29,315 | __main__ | INFO | PPP = 20
+2025-01-08 13:29:29,316 | __main__ | INFO | kSource
+2025-01-08 13:29:31,937 | __main__ | INFO | kSensor
+2025-01-08 13:29:31,939 | __main__ | INFO | simulation_options
+2025-01-08 13:29:31,942 | __main__ | INFO | execution_options
+2025-01-08 13:29:31,945 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 13:32:52,600 | __main__ | INFO | post processing
+2025-01-08 13:32:52,601 | __main__ | INFO | extract_amp_phase
+2025-01-08 13:33:04,249 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 13:33:04,250 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 13:45:49,858 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 13:45:49,866 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 13:45:50,294 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 13:45:50,295 | __main__ | INFO | transducer is planar
+2025-01-08 13:45:50,809 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 13:45:50,811 | __main__ | INFO | not updated dt
+2025-01-08 13:45:50,813 | __main__ | INFO | PPW = 1.875
+2025-01-08 13:45:50,815 | __main__ | INFO | CFL = 20.0
+2025-01-08 13:45:50,816 | __main__ | INFO | PPP = 20
+2025-01-08 13:45:50,818 | __main__ | INFO | kSource
+2025-01-08 13:45:53,646 | __main__ | INFO | kSensor
+2025-01-08 13:45:53,648 | __main__ | INFO | simulation_options
+2025-01-08 13:45:53,649 | __main__ | INFO | execution_options
+2025-01-08 13:45:53,649 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 13:49:16,530 | __main__ | INFO | post processing
+2025-01-08 13:49:16,532 | __main__ | INFO | extract_amp_phase
+2025-01-08 13:49:28,656 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 13:49:28,656 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 13:55:01,523 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 13:55:01,527 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 13:55:01,794 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 13:55:01,795 | __main__ | INFO | transducer is planar
+2025-01-08 13:55:02,184 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 13:55:02,186 | __main__ | INFO | not updated dt
+2025-01-08 13:55:02,186 | __main__ | INFO | PPW = 1.875
+2025-01-08 13:55:02,186 | __main__ | INFO | CFL = 20.0
+2025-01-08 13:55:02,187 | __main__ | INFO | PPP = 20
+2025-01-08 13:55:02,187 | __main__ | INFO | kSource
+2025-01-08 13:55:05,839 | __main__ | INFO | kSensor
+2025-01-08 13:55:05,842 | __main__ | INFO | simulation_options
+2025-01-08 13:55:05,842 | __main__ | INFO | execution_options
+2025-01-08 13:55:05,843 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 13:58:24,567 | __main__ | INFO | post processing
+2025-01-08 13:58:24,568 | __main__ | INFO | extract_amp_phase
+2025-01-08 13:58:35,832 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 13:58:35,834 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 14:02:08,636 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 14:02:08,641 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 14:02:08,940 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 14:02:08,942 | __main__ | INFO | transducer is planar
+2025-01-08 14:02:09,279 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 14:02:09,280 | __main__ | INFO | not updated dt
+2025-01-08 14:02:09,280 | __main__ | INFO | PPW = 1.875
+2025-01-08 14:02:09,282 | __main__ | INFO | CFL = 20.0
+2025-01-08 14:02:09,283 | __main__ | INFO | PPP = 20
+2025-01-08 14:02:09,283 | __main__ | INFO | kSource
+2025-01-08 14:02:12,482 | __main__ | INFO | kSensor
+2025-01-08 14:02:12,485 | __main__ | INFO | simulation_options
+2025-01-08 14:02:12,487 | __main__ | INFO | execution_options
+2025-01-08 14:02:12,488 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 14:05:31,688 | __main__ | INFO | post processing
+2025-01-08 14:05:31,689 | __main__ | INFO | extract_amp_phase
+2025-01-08 14:05:43,555 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 14:05:43,556 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 14:08:16,017 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 14:08:16,020 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 14:08:16,282 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 14:08:16,283 | __main__ | INFO | transducer is planar
+2025-01-08 14:08:16,625 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 14:08:16,625 | __main__ | INFO | not updated dt
+2025-01-08 14:08:16,626 | __main__ | INFO | PPW = 1.875
+2025-01-08 14:08:16,627 | __main__ | INFO | CFL = 20.0
+2025-01-08 14:08:16,628 | __main__ | INFO | PPP = 20
+2025-01-08 14:08:16,629 | __main__ | INFO | kSource
+2025-01-08 14:08:19,610 | __main__ | INFO | kSensor
+2025-01-08 14:08:19,612 | __main__ | INFO | simulation_options
+2025-01-08 14:08:19,614 | __main__ | INFO | execution_options
+2025-01-08 14:08:19,615 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 14:11:38,168 | __main__ | INFO | post processing
+2025-01-08 14:11:38,170 | __main__ | INFO | extract_amp_phase
+2025-01-08 14:11:50,296 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 14:11:50,298 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 14:14:09,881 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 14:14:09,886 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 14:14:10,161 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 14:14:10,162 | __main__ | INFO | transducer is planar
+2025-01-08 14:14:10,535 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 14:14:10,536 | __main__ | INFO | not updated dt
+2025-01-08 14:14:10,537 | __main__ | INFO | PPW = 1.875
+2025-01-08 14:14:10,537 | __main__ | INFO | CFL = 20.0
+2025-01-08 14:14:10,537 | __main__ | INFO | PPP = 20
+2025-01-08 14:14:10,539 | __main__ | INFO | kSource
+2025-01-08 14:14:14,258 | __main__ | INFO | kSensor
+2025-01-08 14:14:14,261 | __main__ | INFO | simulation_options
+2025-01-08 14:14:14,263 | __main__ | INFO | execution_options
+2025-01-08 14:14:14,265 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 14:17:31,882 | __main__ | INFO | post processing
+2025-01-08 14:17:31,883 | __main__ | INFO | extract_amp_phase
+2025-01-08 14:17:47,602 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 14:17:47,604 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 14:24:03,187 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 14:24:03,195 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 14:24:03,507 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 14:24:03,510 | __main__ | INFO | transducer is planar
+2025-01-08 14:24:03,954 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 14:24:03,954 | __main__ | INFO | not updated dt
+2025-01-08 14:24:03,956 | __main__ | INFO | PPW = 1.875
+2025-01-08 14:24:03,957 | __main__ | INFO | CFL = 20.0
+2025-01-08 14:24:03,958 | __main__ | INFO | PPP = 20
+2025-01-08 14:24:03,958 | __main__ | INFO | kSource
+2025-01-08 14:24:07,395 | __main__ | INFO | kSensor
+2025-01-08 14:24:07,397 | __main__ | INFO | simulation_options
+2025-01-08 14:24:07,397 | __main__ | INFO | execution_options
+2025-01-08 14:24:07,400 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 14:27:25,539 | __main__ | INFO | post processing
+2025-01-08 14:27:25,539 | __main__ | INFO | extract_amp_phase
+2025-01-08 14:27:38,297 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 14:27:38,297 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 14:29:19,051 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 14:29:19,053 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 14:29:19,319 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 14:29:19,321 | __main__ | INFO | transducer is planar
+2025-01-08 14:29:19,599 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 14:29:19,601 | __main__ | INFO | not updated dt
+2025-01-08 14:29:19,601 | __main__ | INFO | PPW = 1.875
+2025-01-08 14:29:19,601 | __main__ | INFO | CFL = 20.0
+2025-01-08 14:29:19,601 | __main__ | INFO | PPP = 20
+2025-01-08 14:29:19,601 | __main__ | INFO | kSource
+2025-01-08 14:29:21,678 | __main__ | INFO | kSensor
+2025-01-08 14:29:21,681 | __main__ | INFO | simulation_options
+2025-01-08 14:29:21,681 | __main__ | INFO | execution_options
+2025-01-08 14:29:21,681 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 14:32:38,368 | __main__ | INFO | post processing
+2025-01-08 14:32:38,369 | __main__ | INFO | extract_amp_phase
+2025-01-08 14:32:49,795 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 14:32:49,797 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 14:35:10,560 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 14:35:10,566 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 14:35:10,825 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 14:35:10,826 | __main__ | INFO | transducer is planar
+2025-01-08 14:35:11,167 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 14:35:11,168 | __main__ | INFO | not updated dt
+2025-01-08 14:35:11,169 | __main__ | INFO | PPW = 1.875
+2025-01-08 14:35:11,170 | __main__ | INFO | CFL = 20.0
+2025-01-08 14:35:11,170 | __main__ | INFO | PPP = 20
+2025-01-08 14:35:11,171 | __main__ | INFO | kSource
+2025-01-08 14:35:14,585 | __main__ | INFO | kSensor
+2025-01-08 14:35:14,587 | __main__ | INFO | simulation_options
+2025-01-08 14:35:14,588 | __main__ | INFO | execution_options
+2025-01-08 14:35:14,589 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 14:38:31,666 | __main__ | INFO | post processing
+2025-01-08 14:38:31,667 | __main__ | INFO | extract_amp_phase
+2025-01-08 14:38:45,537 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 14:38:45,539 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 14:54:12,346 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 14:54:12,355 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 14:54:12,756 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 14:54:12,759 | __main__ | INFO | transducer is planar
+2025-01-08 14:54:13,225 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 14:54:13,226 | __main__ | INFO | not updated dt
+2025-01-08 14:54:13,227 | __main__ | INFO | PPW = 1.875
+2025-01-08 14:54:13,228 | __main__ | INFO | CFL = 20.0
+2025-01-08 14:54:13,228 | __main__ | INFO | PPP = 20
+2025-01-08 14:54:13,229 | __main__ | INFO | kSource
+2025-01-08 14:54:16,845 | __main__ | INFO | kSensor
+2025-01-08 14:54:16,847 | __main__ | INFO | simulation_options
+2025-01-08 14:54:16,849 | __main__ | INFO | execution_options
+2025-01-08 14:54:16,851 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 14:57:36,844 | __main__ | INFO | post processing
+2025-01-08 14:57:36,845 | __main__ | INFO | extract_amp_phase
+2025-01-08 14:57:58,221 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 14:57:58,222 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 14:58:58,970 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 14:58:58,974 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 14:58:59,215 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 14:58:59,215 | __main__ | INFO | transducer is planar
+2025-01-08 14:58:59,575 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 14:58:59,576 | __main__ | INFO | not updated dt
+2025-01-08 14:58:59,576 | __main__ | INFO | PPW = 1.875
+2025-01-08 14:58:59,577 | __main__ | INFO | CFL = 20.0
+2025-01-08 14:58:59,578 | __main__ | INFO | PPP = 20
+2025-01-08 14:58:59,579 | __main__ | INFO | kSource
+2025-01-08 14:59:02,454 | __main__ | INFO | kSensor
+2025-01-08 14:59:02,456 | __main__ | INFO | simulation_options
+2025-01-08 14:59:02,458 | __main__ | INFO | execution_options
+2025-01-08 14:59:02,459 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 15:02:19,836 | __main__ | INFO | post processing
+2025-01-08 15:02:19,837 | __main__ | INFO | extract_amp_phase
+2025-01-08 15:02:31,340 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 15:02:31,341 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 15:02:34,253 | __main__ | INFO | focus in brain: (48, 48, 31), mid point: [48, 48, 0] last plane: (48, 48)
+2025-01-08 15:59:19,838 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 15:59:19,841 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 15:59:20,063 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 15:59:20,065 | __main__ | INFO | transducer is planar
+2025-01-08 15:59:20,327 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 15:59:20,328 | __main__ | INFO | not updated dt
+2025-01-08 15:59:20,329 | __main__ | INFO | PPW = 1.875
+2025-01-08 15:59:20,329 | __main__ | INFO | CFL = 20.0
+2025-01-08 15:59:20,329 | __main__ | INFO | PPP = 20
+2025-01-08 15:59:20,330 | __main__ | INFO | kSource
+2025-01-08 15:59:22,336 | __main__ | INFO | kSensor
+2025-01-08 15:59:22,337 | __main__ | INFO | simulation_options
+2025-01-08 15:59:22,337 | __main__ | INFO | execution_options
+2025-01-08 15:59:22,338 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 16:02:38,741 | __main__ | INFO | post processing
+2025-01-08 16:02:38,744 | __main__ | INFO | extract_amp_phase
+2025-01-08 16:02:53,359 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 16:02:53,361 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 16:06:06,195 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 16:06:06,197 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 16:06:06,371 | __main__ | INFO | new shape=(97, 97, 216)
+2025-01-08 16:06:06,371 | __main__ | INFO | transducer is planar
+2025-01-08 16:06:06,618 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 16:06:06,619 | __main__ | INFO | not updated dt
+2025-01-08 16:06:06,619 | __main__ | INFO | PPW = 1.875
+2025-01-08 16:06:06,620 | __main__ | INFO | CFL = 20.0
+2025-01-08 16:06:06,620 | __main__ | INFO | PPP = 20
+2025-01-08 16:06:06,620 | __main__ | INFO | kSource
+2025-01-08 16:06:08,470 | __main__ | INFO | kSensor
+2025-01-08 16:06:08,471 | __main__ | INFO | simulation_options
+2025-01-08 16:06:08,473 | __main__ | INFO | execution_options
+2025-01-08 16:06:08,473 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 16:09:25,267 | __main__ | INFO | post processing
+2025-01-08 16:09:25,268 | __main__ | INFO | extract_amp_phase
+2025-01-08 16:09:37,961 | __main__ | INFO | size of contours:(216, 97), (216, 97), (97, 97).
+2025-01-08 16:09:37,963 | __main__ | INFO | number of contours: (4, 4, 2).
+2025-01-08 16:09:40,667 | __main__ | INFO | focus in brain: (48, 48, 31), mid point: [48, 48, 0] last plane: (48, 48)
+2025-01-08 16:29:39,806 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 16:29:39,814 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 16:29:40,258 | __main__ | INFO | new shape=(97, 97, 210)
+2025-01-08 16:29:40,259 | __main__ | INFO | transducer is planar
+2025-01-08 16:29:40,716 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=6.25e-08
+2025-01-08 16:29:40,718 | __main__ | INFO | not updated dt
+2025-01-08 16:29:40,720 | __main__ | INFO | PPW = 1.875
+2025-01-08 16:29:40,721 | __main__ | INFO | CFL = 20.0
+2025-01-08 16:29:40,723 | __main__ | INFO | PPP = 20
+2025-01-08 16:29:40,726 | __main__ | INFO | kSource
+2025-01-08 16:29:44,828 | __main__ | INFO | kSensor
+2025-01-08 16:29:44,830 | __main__ | INFO | simulation_options
+2025-01-08 16:29:44,834 | __main__ | INFO | execution_options
+2025-01-08 16:29:44,835 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 16:33:01,571 | __main__ | INFO | post processing
+2025-01-08 16:33:01,572 | __main__ | INFO | extract_amp_phase
+2025-01-08 16:33:18,806 | __main__ | INFO | size of contours:(210, 97), (210, 97), (97, 97).
+2025-01-08 16:33:18,809 | __main__ | INFO | number of contours: (4, 4, 3).
+2025-01-08 16:33:23,152 | __main__ | INFO | focus in brain: (48, 48, 26), mid point: [48, 48, 0] last plane: (48, 48)
+2025-01-08 16:35:58,944 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-08 16:35:58,952 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-08 16:35:59,414 | __main__ | INFO | new shape=(97, 97, 210)
+2025-01-08 16:35:59,415 | __main__ | INFO | transducer is planar
+2025-01-08 16:35:59,911 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=1e-07
+2025-01-08 16:35:59,913 | __main__ | INFO | not updated dt
+2025-01-08 16:35:59,915 | __main__ | INFO | PPW = 3.0
+2025-01-08 16:35:59,918 | __main__ | INFO | CFL = 20.0
+2025-01-08 16:35:59,920 | __main__ | INFO | PPP = 20
+2025-01-08 16:35:59,922 | __main__ | INFO | kSource
+2025-01-08 16:36:05,000 | __main__ | INFO | kSensor
+2025-01-08 16:36:05,004 | __main__ | INFO | simulation_options
+2025-01-08 16:36:05,006 | __main__ | INFO | execution_options
+2025-01-08 16:36:05,009 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-08 16:38:11,632 | __main__ | INFO | post processing
+2025-01-08 16:38:11,633 | __main__ | INFO | extract_amp_phase
+2025-01-08 16:38:27,423 | __main__ | INFO | size of contours:(210, 97), (210, 97), (97, 97).
+2025-01-08 16:38:27,425 | __main__ | INFO | number of contours: (4, 4, 3).
+2025-01-08 16:38:29,743 | __main__ | INFO | focus in brain: (50, 50, 16), mid point: [48, 48, 0] last plane: (44, 47)
+2025-01-09 14:22:23,733 | __main__ | INFO | C:/Users/dsinden/Documents/GitLab/k-wave-python/data/skull_mask_bm8_dx_1mm.mat
+2025-01-09 14:22:23,733 | __main__ | INFO | ['brain_mask', 'dx', 'skull_mask', 'xi', 'yi', 'zi']
+2025-01-09 14:22:23,890 | __main__ | INFO | new shape=(97, 97, 210)
+2025-01-09 14:22:23,890 | __main__ | INFO | transducer is planar
+2025-01-09 14:22:24,080 | __main__ | INFO | dt_stability_limit=1.7208334500763013e-07, dt=1e-07
+2025-01-09 14:22:24,080 | __main__ | INFO | not updated dt
+2025-01-09 14:22:24,080 | __main__ | INFO | PPW = 3.0
+2025-01-09 14:22:24,080 | __main__ | INFO | CFL = 20.0
+2025-01-09 14:22:24,080 | __main__ | INFO | PPP = 20
+2025-01-09 14:22:24,080 | __main__ | INFO | kSource
+2025-01-09 14:22:25,600 | __main__ | INFO | kSensor
+2025-01-09 14:22:25,600 | __main__ | INFO | simulation_options
+2025-01-09 14:22:25,600 | __main__ | INFO | execution_options
+2025-01-09 14:22:25,600 | __main__ | INFO | kspaceFirstOrder3DG
+2025-01-09 14:24:29,363 | __main__ | INFO | post processing
+2025-01-09 14:24:29,363 | __main__ | INFO | extract_amp_phase
+2025-01-09 14:24:40,963 | __main__ | INFO | size of contours:(210, 97), (210, 97), (97, 97).
+2025-01-09 14:24:40,963 | __main__ | INFO | number of contours: (4, 4, 3).
+2025-01-09 14:24:42,841 | __main__ | INFO | focus in brain: (50, 50, 16), mid point: [48, 48, 0] last plane: (44, 47)
diff --git a/examples/ewp_3D_simulation/ewp_3D_simulation.py b/examples/ewp_3D_simulation/ewp_3D_simulation.py
new file mode 100644
index 000000000..f6caf913c
--- /dev/null
+++ b/examples/ewp_3D_simulation/ewp_3D_simulation.py
@@ -0,0 +1,460 @@
+
+import numpy as np
+import matplotlib.pyplot as plt
+from copy import deepcopy
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.ksensor import kSensor
+from kwave.pstdElastic3D import pstd_elastic_3d
+
+from kwave.utils.signals import tone_burst
+from kwave.utils.colormap import get_color_map
+
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+
+import pyvista as pv
+from skimage import measure
+# import xarray as xa
+
+
+def focus(kgrid, input_signal, source_mask, focus_position, sound_speed):
+ """
+ focus Create input signal based on source mask and focus position.
+
+ focus takes a single input signal and a source mask and creates an
+ input signal matrix (with one input signal for each source point).
+ The appropriate time delays required to focus the signals at a given
+ position in Cartesian space are automatically added based on the user
+ inputs for focus_position and sound_speed.
+
+ Args:
+ kgrid: k-Wave grid object returned by kWaveGrid
+ input_signal: single time series input
+ source_mask: matrix specifying the positions of the time
+ varying source distribution (i.e., source.p_mask
+ or source.u_mask)
+ focus_position: position of the focus in Cartesian coordinates
+ sound_speed: scalar sound speed
+
+ Returns:
+ input_signal_mat: matrix of time series following the source points
+ """
+
+ assert not isinstance(kgrid.t_array, str), "kgrid.t_array must be a numeric array."
+
+ if isinstance(sound_speed, int):
+ sound_speed = float(sound_speed)
+
+ assert isinstance(sound_speed, float), "sound_speed must be a scalar."
+
+ # calculate the distance from every point in the source mask to the focus position
+ if kgrid.dim == 1:
+ dist = np.abs(kgrid.x[source_mask == 1] - focus_position[0])
+ elif kgrid.dim == 2:
+ dist = np.sqrt((kgrid.x[source_mask == 1] - focus_position[0])**2 +
+ (kgrid.y[source_mask == 1] - focus_position[1])**2 )
+ elif kgrid.dim == 3:
+ dist = np.sqrt((kgrid.x[source_mask == 1] - focus_position[0])**2 +
+ (kgrid.y[source_mask == 1] - focus_position[1])**2 +
+ (kgrid.z[source_mask == 1] - focus_position[2])**2 )
+
+ # convert distances to time delays
+ delays = np.round(dist / (kgrid.dt * sound_speed)).astype(int)
+
+ # convert time points to delays relative to the maximum delays
+ relative_delays = delays.max() - delays
+
+ # largest time delay
+ max_delay = np.max(relative_delays)
+
+ signal_mat = np.zeros((relative_delays.size, input_signal.size + max_delay), order='F')
+
+ # assign the input signal
+ for source_index, delay in enumerate(relative_delays):
+ signal_mat[source_index, :] = np.hstack([np.zeros((delay,)),
+ np.squeeze(input_signal),
+ np.zeros((max_delay - delay,))])
+
+ return signal_mat
+
+
+def get_focus(p):
+ """
+ Gets value of maximum pressure and the indices of the location
+ """
+ max_pressure = np.max(p)
+ mx, my, mz = np.unravel_index(np.argmax(p, axis=None), p.shape)
+ return max_pressure, [mx, my, mz]
+
+
+def getPVImageData(kgrid, p, order='F'):
+ """Create the pyvista image data container with data label hardwired"""
+ pv_grid = pv.ImageData()
+ pv_grid.dimensions = (kgrid.Nx, kgrid.Ny, kgrid.Nz)
+ pv_grid.origin = (0, 0, 0)
+ pv_grid.spacing = (kgrid.dx, kgrid.dy, kgrid.dz)
+ pv_grid.point_data["p_max"] = p.flatten(order=order)
+ pv_grid.deep_copy = False
+ return pv_grid
+
+
+def getIsoVolume(kgrid, p, dB=-6):
+ """"Returns a triangulation of a volume, warning: may not be connected or closed"""
+ max_pressure, _ = get_focus(p)
+ ratio = 10**(dB / 20.0) * max_pressure
+ # don't need normals or values
+ verts, faces, _, _ = measure.marching_cubes(p, level=ratio, spacing=[kgrid.dx, kgrid.dy, kgrid.dz])
+ return verts, faces
+
+
+def getFWHM(kgrid, p, verbose: bool = False):
+ """"Gets volume of -6dB field"""
+ verts, faces = getIsoVolume(kgrid, p)
+
+ totalArea: float = 0.0
+
+ m: int = np.max(np.shape(faces)) - 1
+ for i in np.arange(0, m, dtype=int):
+ p0 = verts[faces[m, 0]]
+ p1 = verts[faces[m, 1]]
+ p2 = verts[faces[m, 2]]
+
+ a = np.asarray(p1 - p0)
+ b = np.asarray(p2 - p0)
+
+ n = np.cross(a, b)
+ nn = np.abs(n)
+
+ area = nn / 2.0
+ normal = n / nn
+ centre = (p0 + p1 + p2) / 3.0
+
+ totalArea += area * (centre[0] * normal[0] + centre[1] * normal[1] + centre[2] * normal[2])
+
+ d13 = [[verts[faces[:, 1], 0] - verts[faces[:, 2], 0]],
+ [verts[faces[:, 1], 1] - verts[faces[:, 2], 1]],
+ [verts[faces[:, 1], 2] - verts[faces[:, 2], 2]] ]
+
+ d12 = [[verts[faces[:, 0], 0] - verts[faces[:, 1], 0]],
+ [verts[faces[:, 0], 1] - verts[faces[:, 1], 1]],
+ [verts[faces[:, 0], 2] - verts[faces[:, 1], 2]] ]
+
+ # cross-product vectorized
+ cr = np.cross(np.squeeze(np.transpose(d13)), np.squeeze(np.transpose(d12)))
+ cr = np.transpose(cr)
+
+ # Area of each triangle
+ area = 0.5 * np.sqrt(cr[0, :]**2 + cr[1, :]**2 + cr[2, :]**2)
+
+ # Total area
+ totalArea = np.sum(area)
+
+ # norm of cross product
+ crNorm = np.sqrt(cr[0, :]**2 + cr[1, :]**2 + cr[2, :]**2)
+
+ # centroid
+ zMean = (verts[faces[:, 0], 2] + verts[faces[:, 1], 2] + verts[faces[:, 2], 2]) / 3.0
+
+ # z component of normal for each triangle
+ nz = -cr[2, :] / crNorm
+
+ # contribution of each triangle
+ volume = np.abs(np.multiply(np.multiply(area, zMean), nz))
+
+ # divergence theorem
+ totalVolume = np.sum(volume)
+
+ # display volume to screen
+ if verbose:
+ print('\n\tTotal volume of FWHM {vol:8.5e} [m^3]'.format(vol=totalVolume))
+
+ return verts, faces
+
+
+def plot3D(kgrid, p, tx_plane_coords, verbose=False):
+ """Plots using pyvista"""
+
+ max_pressure, max_loc = get_focus(p)
+ if verbose:
+ print(max_pressure, max_loc)
+
+ min_pressure = np.min(p)
+ if verbose:
+ print(min_pressure)
+
+ pv_grid = getPVImageData(kgrid, p)
+ if verbose:
+ print(pv_grid)
+
+ verts, faces = getFWHM(kgrid, p)
+
+ num_faces = faces.shape[0]
+ faces_pv = np.hstack([np.full((num_faces, 1), 3), faces])
+
+ dataset = pv.PolyData(verts, faces_pv)
+
+ pv_x = np.linspace(0, (kgrid.Nx - 1.0) * kgrid.dx, kgrid.Nx)
+ pv_y = np.linspace(0, (kgrid.Ny - 1.0) * kgrid.dy, kgrid.Ny)
+ pv_z = np.linspace(0, (kgrid.Nz - 1.0) * kgrid.dz, kgrid.Nz)
+
+ islands = dataset.connectivity(largest=False)
+ split_islands = islands.split_bodies(label=True)
+ region = []
+ xx = []
+ for i, body in enumerate(split_islands):
+ region.append(body)
+ pntdata = body.GetPoints()
+ xx.append(np.zeros((pntdata.GetNumberOfPoints(), 3)))
+ for j in range(pntdata.GetNumberOfPoints()):
+ xx[i][j, 0] = pntdata.GetPoint(j)[0]
+ xx[i][j, 1] = pntdata.GetPoint(j)[1]
+ xx[i][j, 2] = pntdata.GetPoint(j)[2]
+
+ tx_plane = [pv_x[tx_plane_coords[0]],
+ pv_y[tx_plane_coords[1]],
+ pv_z[tx_plane_coords[2]]]
+
+ mx, my, mz = max_loc
+ max_loc = [pv_x[mx], pv_y[my], pv_z[mz]]
+
+ single_slice_x = pv_grid.slice(origin=max_loc, normal=[1, 0, 0])
+ single_slice_y = pv_grid.slice(origin=max_loc, normal=[0, 1, 0])
+ single_slice_z = pv_grid.slice(origin=max_loc, normal=[0, 0, 1])
+
+ single_slice_tx = pv_grid.slice(origin=tx_plane, normal=[1, 0, 0])
+
+ # formatting of colorbar
+ sargs = dict(interactive=True,
+ title='Pressure [Pa]',
+ height=0.90,
+ vertical=True,
+ position_x=0.90,
+ position_y=0.05,
+ title_font_size=20,
+ label_font_size=16,
+ shadow=False,
+ n_labels=6,
+ italic=False,
+ fmt="%.5e",
+ font_family="arial")
+
+ # dictionary for annotations of colorbar
+ ratio = 10**(-6 / 20.0) * max_pressure
+
+ annotations = dict([(float(ratio), "-6dB")])
+
+ # plotter object
+ plotter = pv.Plotter()
+
+ # slice data
+ _ = plotter.add_mesh(single_slice_x,
+ cmap='turbo',
+ clim=[min_pressure, max_pressure],
+ opacity=0.5,
+ scalar_bar_args=sargs,
+ annotations=annotations)
+ _ = plotter.add_mesh(single_slice_y, cmap='turbo', clim=[min_pressure, max_pressure], opacity=0.5, show_scalar_bar=False)
+ _ = plotter.add_mesh(single_slice_z, cmap='turbo', clim=[min_pressure, max_pressure], opacity=0.5, show_scalar_bar=False)
+
+ # transducer plane
+ _ = plotter.add_mesh(single_slice_tx, cmap='spring', clim=[min_pressure, max_pressure], opacity=1, show_scalar_bar=False)
+
+ # full width half maximum
+ _ = plotter.add_mesh(region[0], color='red', opacity=0.75, label='-6 dB')
+
+ # add the frame around the image
+ _ = plotter.show_bounds(grid='front',
+ location='outer',
+ ticks='outside',
+ color='black',
+ minor_ticks=False,
+ padding=0.0,
+ show_xaxis=True,
+ show_xlabels=True,
+ xtitle='',
+ n_xlabels=5,
+ ytitle="",
+ ztitle="")
+
+ _ = plotter.add_axes(color='pink', labels_off=False)
+
+ plotter.show()
+
+
+"""
+Simulations In Three Dimensions Example
+
+This example provides a simple demonstration of using k-Wave to model
+elastic waves in a three-dimensional heterogeneous propagation medium. It
+builds on the Explosive Source In A Layered Medium and Simulations In
+Three-Dimensions examples.
+
+author: Bradley Treeby
+date: 14th February 2014
+last update: 29th May 2017
+
+This function is part of the k-Wave Toolbox (http://www.k-wave.org)
+Copyright (C) 2014-2017 Bradley Treeby
+
+This file is part of k-Wave. k-Wave is free software: you can
+redistribute it and/or modify it under the terms of the GNU Lesser
+General Public License as published by the Free Software Foundation,
+either version 3 of the License, or (at your option) any later version.
+
+k-Wave is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with k-Wave. If not, see .
+"""
+
+
+# =========================================================================
+# SIMULATION
+# =========================================================================
+
+# Fortran ordering
+myOrder = 'F'
+
+# set size of perfectly matched layer
+pml_size: int = 10
+
+# set grid properties
+Nx: int = 64
+Ny: int = 64
+Nz: int = 64
+dx: float = 0.1e-3
+dy: float = 0.1e-3
+dz: float = 0.1e-3
+kgrid = kWaveGrid(Vector([Nx, Ny, Nz]), Vector([dx, dy, dz]))
+
+# define the properties of the upper layer of the propagation medium
+c0: float = 1500.0 # [m/s]
+rho0: float = 1000.0 # [kg/m^3]
+sound_speed_compression = c0 * np.ones((Nx, Ny, Nz), order=myOrder) # [m/s]
+sound_speed_shear = np.zeros((Nx, Ny, Nz), order=myOrder) # [m/s]
+density = rho0 * np.ones((Nx, Ny, Nz), order=myOrder) # [kg/m^3]
+
+# define the properties of the lower layer of the propagation medium
+sound_speed_compression[Nx // 2 - 1:, :, :] = 2000.0 # [m/s]
+sound_speed_shear[Nx // 2 - 1:, :, :] = 800.0 # [m/s]
+density[Nx // 2 - 1:, :, :] = 1200.0 # [kg/m^3]
+
+# define the absorption properties
+alpha_coeff_compression = 0.1 # [dB/(MHz^2 cm)]
+alpha_coeff_shear = 0.5 # [dB/(MHz^2 cm)]
+
+medium = kWaveMedium(sound_speed_compression,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear,
+ density=density,
+ alpha_coeff_compression=alpha_coeff_compression,
+ alpha_coeff_shear=alpha_coeff_shear)
+
+# create the time array
+cfl: float = 0.1 # Courant-Friedrichs-Lewy number
+t_end: float = 5e-6 # [s]
+kgrid.makeTime(np.max(medium.sound_speed_compression.flatten()), cfl, t_end)
+
+# define source mask to be a square piston
+source = kSource()
+source_x_pos: int = 10 # [grid points]
+source_radius: int = 15 # [grid points]
+source.u_mask = np.zeros((Nx, Ny, Nz), dtype=int, order=myOrder)
+source.u_mask[source_x_pos,
+ Ny // 2 - source_radius:Ny // 2 + source_radius,
+ Nz // 2 - source_radius:Nz // 2 + source_radius] = 1
+
+# define source to be a velocity source
+source_freq = 2e6 # [Hz]
+source_cycles = 3 # []
+source_mag = 1e-6 # [m/s]
+fs = 1.0 / kgrid.dt # [Hz]
+ux = source_mag * tone_burst(fs, source_freq, source_cycles)
+
+# set source focus
+source.ux = focus(kgrid, ux, source.u_mask, [0, 0, 0], c0)
+
+# define sensor mask in x-y plane using cuboid corners, where a rectangular
+# mask is defined using the xyz coordinates of two opposing corners in the
+# form [x1, y1, z1, x2, y2, z2].'
+# In this case the sensor mask in the slice through the xy-plane at z = Nz // 2 - 1
+# cropping the pml
+sensor = kSensor()
+sensor.mask = np.array([[pml_size, pml_size, Nz // 2 - 1, Nx - pml_size, Ny - pml_size, Nz // 2]]).T
+
+# record the maximum pressure in the sensor.mask plane
+sensor.record = ['p_max']
+
+# define input arguments
+simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ kelvin_voigt_model=True,
+ use_sensor=True,
+ nonuniform_grid=False,
+ blank_sensor=False)
+
+# run the simulation
+sensor_data = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+
+# =========================================================================
+# VISUALISATION
+# =========================================================================
+
+# data = xa.DataArray(sensor_data.p_max,
+# dims=("x", "y", "z"),
+# coords={"x": [10, 20]}
+# attrs={'units':'Pa', 'spatial_units':'m', 'temporal_units':'s'})
+
+# sz = list(params.coords.sizes.values())
+# p_max = xa.DataArray(output['p_max'].reshape(sz, order='F'),
+# coords=params.coords,
+# name='p_max',
+# attrs={'units':'Pa', 'long_name':'PPP'})
+
+
+
+# ds = xa.Dataset({'p_max':p_max,
+# 'fwhm':fwhm,
+# 'beamaxis': beamaxis})
+
+# define axes
+x_vec = np.squeeze(kgrid.x_vec) * 1000.0
+y_vec = np.squeeze(kgrid.y_vec) * 1000.0
+x_vec = x_vec[pml_size:Nx - pml_size]
+y_vec = y_vec[pml_size:Ny - pml_size]
+
+# p_max_f = np.reshape(sensor_data[0].p_max, (x_vec.size, y_vec.size), order='F')
+# p_max_c = np.reshape(sensor_data[0].p_max, (x_vec.size, y_vec.size), order='C')
+
+# sensor_data.p_max = np.reshape(sensor_data.p_max, (Nx, Ny, Nz), order='F')
+
+# p_max = np.reshape(sensor_data.p_max[pml_size:Nx - pml_size, pml_size:Ny - pml_size, Nz // 2 - 1], (x_vec.size, y_vec.size), order='F')
+
+p_max = np.reshape(sensor_data[0].p_max, (x_vec.size, y_vec.size), order='F')
+
+# plot
+fig1, ax1 = plt.subplots(nrows=1, ncols=1)
+pcm1 = ax1.pcolormesh(x_vec, y_vec, p_max,
+ cmap = get_color_map(), shading='gouraud', alpha=1.0, vmin=0, vmax=6)
+cb1 = fig1.colorbar(pcm1, ax=ax1)
+ax1.set_ylabel('$x$ [mm]')
+ax1.set_xlabel('$y$ [mm]')
+
+plt.show()
+
+# # indices of transducer location
+# coordinates = np.argwhere(source.u_mask == 1)
+# coordinates = np.reshape(coordinates, (-1, 3))
+
+# # 3D plotting
+# plot3D(kgrid, sensor_data.p_max, coordinates[0])
\ No newline at end of file
diff --git a/examples/ewp_layered_medium/ewp_layered_medium.py b/examples/ewp_layered_medium/ewp_layered_medium.py
new file mode 100644
index 000000000..ac7f5d1c1
--- /dev/null
+++ b/examples/ewp_layered_medium/ewp_layered_medium.py
@@ -0,0 +1,196 @@
+
+import numpy as np
+import matplotlib.pyplot as plt
+# from matplotlib import colors
+# from matplotlib.animation import FuncAnimation
+from copy import deepcopy
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.ksensor import kSensor
+from kwave.pstdElastic2D import pstd_elastic_2d
+
+from kwave.utils.dotdictionary import dotdict
+from kwave.utils.mapgen import make_disc, make_circle
+from kwave.utils.signals import reorder_sensor_data
+
+from kwave.utils.colormap import get_color_map
+
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+
+
+"""
+Explosive Source In A Layered Medium Example
+
+This example provides a simple demonstration of using k-Wave for the
+simulation and detection of compressional and shear waves in elastic and
+viscoelastic media within a two-dimensional heterogeneous medium. It
+builds on the Homogenous Propagation Medium and Heterogeneous Propagation
+Medium examples.
+
+author: Bradley Treeby
+date: 11th February 2014
+last update: 29th May 2017
+
+This function is part of the k-Wave Toolbox (http://www.k-wave.org)
+Copyright (C) 2014-2017 Bradley Treeby
+
+This file is part of k-Wave. k-Wave is free software: you can
+redistribute it and/or modify it under the terms of the GNU Lesser
+General Public License as published by the Free Software Foundation,
+either version 3 of the License, or (at your option) any later version.
+
+k-Wave is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with k-Wave. If not, see .
+"""
+
+
+
+# =========================================================================
+# SIMULATION
+# =========================================================================
+
+# create the computational grid
+Nx: int = 128 # number of grid points in the x (row) direction
+Ny: int = 128 # number of grid points in the y (column) direction
+dx: float = 0.1e-3 # grid point spacing in the x direction [m]
+dy: float = 0.1e-3 # grid point spacing in the y direction [m]
+kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))
+
+# define the properties of the upper layer of the propagation medium
+sound_speed_compression = 1500.0 * np.ones((Nx, Ny)) # [m/s]
+sound_speed_shear = np.zeros((Nx, Ny)) # [m/s]
+density = 1000.0 * np.ones((Nx, Ny)) # [kg/m^3]
+
+# define the properties of the lower layer of the propagation medium
+sound_speed_compression[Nx // 2 - 1:, :] = 2000.0 # [m/s]
+sound_speed_shear[Nx // 2 - 1:, :] = 800.0 # [m/s]
+density[Nx // 2 - 1:, :] = 1200.0 # [kg/m^3]
+
+# define the absorption properties
+alpha_coeff_compression = 0.1 # [dB/(MHz^2 cm)]
+alpha_coeff_shear = 0.5 # [dB/(MHz^2 cm)]
+
+medium = kWaveMedium(sound_speed_compression,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear,
+ density=density,
+ alpha_coeff_compression=alpha_coeff_compression,
+ alpha_coeff_shear=alpha_coeff_shear)
+
+# create the time array
+cfl: float = 0.1 # Courant-Friedrichs-Lewy number
+t_end: float = 8e-6 # [s]
+kgrid.makeTime(np.max(medium.sound_speed_compression.flatten()), cfl, t_end)
+
+# create initial pressure distribution using make_disc
+disc_magnitude: float = 5.0 # [Pa]
+disc_x_pos: int = 29 # [grid points]
+disc_y_pos: int = 63 # [grid points]
+disc_radius: int = 5 # [grid points]
+source = kSource()
+source.p0 = disc_magnitude * make_disc(Vector([Nx, Ny]), Vector([disc_x_pos, disc_y_pos]), disc_radius)
+
+# define a circular sensor or radius 20 grid points, centred at origin
+sensor = kSensor()
+sensor_x_pos: int = Nx // 2 - 1 # [grid points]
+sensor_y_pos: int = Ny // 2 - 1 # [grid points]
+sensor_radius: int = 20 # [grid points]
+sensor.mask = make_circle(Vector([Nx, Ny]), Vector([sensor_x_pos, sensor_y_pos]), sensor_radius)
+sensor.record = ['p']
+
+# define a custom display mask showing the position of the interface from
+# the fluid side
+display_mask = np.zeros((Nx, Ny), dtype=bool)
+display_mask[Nx // 2 - 2, :] = True
+
+# run the simulation
+simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=True)
+
+sensor_data = pstd_elastic_2d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+# reorder the simulation data
+sensor_data_reordered = dotdict()
+sensor_data_reordered.p = reorder_sensor_data(kgrid, sensor, sensor_data.p)
+
+# =========================================================================
+# VISUALISATION
+# =========================================================================
+
+# # Normalize frames based on the maximum value over all frames
+# max_value = np.max(sensor_data_reordered.p)
+# normalized_frames = sensor_data_reordered.p / max_value
+
+# cmap = get_color_map()
+
+# # Create a figure and axis
+# fig0, ax0 = plt.subplots()
+
+# # Create an empty image with the first normalized frame
+# image = ax0.imshow(normalized_frames[0], cmap=cmap, norm=colors.Normalize(vmin=0, vmax=1))
+
+# # Function to update the image for each frame
+# def update(frame):
+# image.set_data(normalized_frames[frame])
+# ax0.set_title(f"Frame {frame + 1}/{kgrid.Nt}")
+# return [image]
+
+# # Create the animation
+# ani = FuncAnimation(fig, update, frames=kgrid.Nt, interval=100) # Adjust interval as needed (in milliseconds)
+
+# # Save the animation as a video file (e.g., MP4)
+# video_filename = "output_video1.mp4"
+# ani.save("./" + video_filename, writer="ffmpeg", fps=30) # Adjust FPS as needed
+
+# # Show the animation (optional)
+# plt.show()
+
+
+# plot layout of simulation
+# fig0, ax0 = plt.subplots(nrows=1, ncols=1)
+# _ = ax0.imshow(kgrid.y.T, kgrid.x.T,
+# np.logical_or(np.logical_or(source.p0, sensor.mask), display_mask).T,
+# cmap='gray_r', interpolation='flat', alpha=1)
+# ax0.invert_yaxis()
+# ax0.set_xlabel('y [mm]')
+# ax0.set_ylabel('x [mm]')
+
+
+fig1, ax1 = plt.subplots(nrows=1, ncols=1)
+_ = ax1.pcolormesh(kgrid.y.T, kgrid.x.T,
+ np.logical_or(np.logical_or(source.p0, sensor.mask), display_mask).T,
+ cmap='gray_r', shading='nearest', alpha=1)
+ax1.invert_yaxis()
+ax1.set_xlabel('y [mm]')
+ax1.set_ylabel('x [mm]')
+
+# time vector
+t_array = np.arange(0, int(kgrid.Nt))
+
+# number of sensors in grid
+n: int = int(np.size(sensor_data_reordered.p) / int(kgrid.Nt))
+
+# sensor vector
+sensors = np.arange(0, int(n))
+
+fig2, ax2 = plt.subplots(nrows=1, ncols=1)
+pcm2 = ax2.pcolormesh(t_array, sensors, -sensor_data_reordered.p, cmap = get_color_map(),
+ shading='gouraud', alpha=1, vmin=-1.0, vmax=1.0)
+ax2.invert_yaxis()
+cb2 = fig2.colorbar(pcm2, ax=ax2)
+ax2.set_ylabel('Sensor Position')
+ax2.set_xlabel('Time Step')
+
+plt.show()
\ No newline at end of file
diff --git a/examples/ewp_plane_wave_absorption/ewp_plane_wave_absorption.py b/examples/ewp_plane_wave_absorption/ewp_plane_wave_absorption.py
new file mode 100644
index 000000000..dddab9981
--- /dev/null
+++ b/examples/ewp_plane_wave_absorption/ewp_plane_wave_absorption.py
@@ -0,0 +1,249 @@
+
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.gridspec as gridspec
+
+from copy import deepcopy
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.ksensor import kSensor
+from kwave.pstdElastic2D import pstd_elastic_2d
+
+from kwave.utils.filters import smooth, spect
+from kwave.utils.math import find_closest
+
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+
+"""
+Plane Wave Absorption Example
+#
+# This example illustrates the characteristics of the Kelvin-Voigt
+# absorption model used in the k-Wave simulation functions pstdElastic2D,
+# pstdElastic3D. It builds on the Explosive Source In A Layered Medium
+# Example.
+#
+# author: Bradley Treeby
+# date: 17th January 2014
+# last update: 25th July 2019
+#
+# This function is part of the k-Wave Toolbox (http://www.k-wave.org)
+# Copyright (C) 2014-2019 Bradley Treeby
+
+# This file is part of k-Wave. k-Wave is free software: you can
+# redistribute it and/or modify it under the terms of the GNU Lesser
+# General Public License as published by the Free Software Foundation,
+# either version 3 of the License, or (at your option) any later version.
+#
+# k-Wave is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with k-Wave. If not, see .
+"""
+
+
+# =========================================================================
+# SET GRID PARAMETERS
+# =========================================================================
+
+# create the computational grid
+Nx: int = 128 # number of grid points in the x (row) direction
+Ny: int = 32 # number of grid points in the y (column) direction
+dx: float = 0.1e-3 # grid point spacing in the x direction [m]
+dy: float = 0.1e-3 # grid point spacing in the y direction [m]
+kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))
+
+# define the properties of the propagation medium
+sound_speed_compression = 1800.0 # [m/s]
+sound_speed_shear = 1200.0 # [m/s]
+density = 1000.0 # [kg/m^3]
+
+# set the absorption properties
+alpha_coeff_compression = 1.0 # [dB/(MHz^2 cm)]
+alpha_coeff_shear = 1.0 # [dB/(MHz^2 cm)]
+
+medium = kWaveMedium(sound_speed=sound_speed_compression,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear,
+ density=density,
+ alpha_coeff_compression=alpha_coeff_compression,
+ alpha_coeff_shear=alpha_coeff_shear)
+
+# define binary sensor mask with two sensor positions
+sensor = kSensor()
+sensor.mask = np.zeros((Nx, Ny), dtype=bool)
+pos1: int = 44 # [grid points]
+pos2: int = 64 # [grid points]
+sensor.mask[pos1, Ny // 2 - 1] = True
+sensor.mask[pos2, Ny // 2 - 1] = True
+
+# set sensor to record to particle velocity
+sensor.record = ["u"]
+
+# calculate the distance between the sensor positions
+d_cm: float = (pos2 - pos1) * dx * 100.0 # [cm]
+
+# define source mask
+source_mask = np.ones((Nx, Ny))
+source_pos: int = 34 # [grid points]
+
+# set the CFL
+cfl: float = 0.05
+
+# define the properties of the PML to allow plane wave propagation
+pml_alpha: float = 0.0
+pml_size = [int(30), int(2)]
+
+# =========================================================================
+# COMPRESSIONAL PLANE WAVE SIMULATION
+# =========================================================================
+
+# define source
+source = kSource()
+source.u_mask = source_mask
+source.ux = np.zeros((Nx, Ny))
+source.ux[source_pos, :] = 1.0
+source.ux = smooth(source.ux, restore_max=True)
+# consistent shape: the source is of shape ((Nx*Ny, 1))
+source.ux = 1e-6 * np.reshape(source.ux, (-1, 1), order='F')
+
+# set end time
+t_end = 3.5e-6
+
+# create a time array
+c_max = np.max([medium.sound_speed_compression, medium.sound_speed_shear])
+kgrid.makeTime(c_max, cfl, t_end)
+
+simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=True,
+ pml_size=pml_size,
+ pml_alpha=pml_alpha,
+ kelvin_voigt_model=True,
+ binary_sensor_mask=True,
+ use_sensor=True,
+ nonuniform_grid=False,
+ blank_sensor=False)
+
+# run the simulation
+sensor_data_comp = pstd_elastic_2d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+# calculate the amplitude spectrum at the two sensor positions as as1 and as2
+fs = 1.0 / kgrid.dt
+_, as1, _ = spect(np.expand_dims(sensor_data_comp.ux[0, :], axis=0), fs)
+f_comp, as2, _ = spect(np.expand_dims(sensor_data_comp.ux[1, :], axis=0), fs)
+
+# calculate the attenuation from the amplitude spectrums
+attenuation_comp = -20.0 * np.log10(as2 / as1) / d_cm
+
+# calculate the corresponding theoretical attenuation in dB/cm
+attenuation_th_comp = medium.alpha_coeff_compression * (f_comp * 1e-6)**2
+
+# calculate the maximum supported frequency
+f_max_comp = medium.sound_speed_compression / (2.0 * dx)
+
+# find the maximum frequency in the frequency vector
+_, f_max_comp_index = find_closest(f_comp, f_max_comp)
+
+# =========================================================================
+# SHEAR PLANE WAVE SIMULATION
+# =========================================================================
+
+# redefine source
+del source
+
+source = kSource()
+source.u_mask = source_mask
+source.uy = np.zeros((Nx, Ny))
+source.uy[source_pos, :] = 1.0
+source.uy = smooth(source.uy, restore_max=True)
+# consistent shape: the source is of shape ((Nx*Ny, 1))
+source.uy = 1e-6 * np.reshape(source.uy, (-1, 1), order='F')
+
+# set end time
+t_end: float = 4e-6
+
+# create a time array
+c_max = np.max([medium.sound_speed_compression, medium.sound_speed_shear])
+kgrid.makeTime(c_max, cfl, t_end)
+
+# run the simulation
+sensor_data_shear = pstd_elastic_2d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+# calculate the amplitude at the two sensor positions
+fs = 1.0 / kgrid.dt
+_, as1, _ = spect(np.expand_dims(sensor_data_shear.uy[0, :], axis=0), fs)
+f_shear, as2, _ = spect(np.expand_dims(sensor_data_shear.uy[1, :], axis=0), fs)
+
+# calculate the attenuation from the amplitude spectrums
+attenuation_shear = -20.0 * np.log10(as2 / as1) / d_cm
+
+# calculate the corresponding theoretical attenuation in dB/cm
+attenuation_th_shear = medium.alpha_coeff_shear * (f_shear * 1e-6)**2
+
+# calculate the maximum supported frequency
+f_max_shear = medium.sound_speed_shear / (2.0 * dx)
+
+# find the maximum frequency in the frequency vector
+_, f_max_shear_index = find_closest(f_shear, f_max_shear)
+
+
+# =========================================================================
+# VISUALISATION
+# =========================================================================
+
+# plot layout of simulation, with padding between compressional and shear plots
+gs = gridspec.GridSpec(5, 1, height_ratios=[1, 1, 0.3, 1, 1])
+ax1 = plt.subplot(gs[0])
+ax2 = plt.subplot(gs[1])
+ax3 = plt.subplot(gs[3])
+ax4 = plt.subplot(gs[4])
+
+# plot compressional wave traces
+t_axis_comp = np.arange(np.shape(sensor_data_comp.ux)[1]) * kgrid.dt * 1e6
+ax1.plot(t_axis_comp, sensor_data_comp.ux[1, :], 'k-')
+ax1.plot(t_axis_comp, sensor_data_comp.ux[0, :], 'k-')
+ax1.set_xlabel(r'Time [$\mu$s]')
+ax1.set_ylabel('Particle Velocity')
+ax1.set_title('Compressional Wave', fontweight='bold')
+
+# plot compressional wave absorption
+ax2.plot(f_comp * 1e-6, np.squeeze(attenuation_th_comp), 'k-', )
+ax2.plot(f_comp * 1e-6, np.squeeze(attenuation_comp), 'o',
+ markeredgecolor='k', markerfacecolor='None')
+ax2.set_xlim(0, f_max_comp * 1e-6)
+ax2.set_ylim(0, attenuation_th_comp[f_max_comp_index] * 1.1)
+ax2.set_xlabel('Frequency [MHz]')
+ax2.set_ylabel(r'$\alpha$ [dB/cm]')
+
+# plot shear wave traces
+t_axis_shear = np.arange(np.shape(sensor_data_shear.uy)[1]) * kgrid.dt * 1e6
+ax3.plot(t_axis_shear, sensor_data_shear.uy[1, :], 'k-')
+ax3.plot(t_axis_shear, sensor_data_shear.uy[0, :], 'k-')
+ax3.set_xlabel(r'Time [$\mu$s]')
+ax3.set_ylabel('Particle Velocity')
+ax3.set_title('Shear Wave', fontweight='bold')
+
+# plot shear wave absorption
+ax4.plot(f_shear * 1e-6, np.squeeze(attenuation_th_shear), 'k-')
+ax4.plot(f_shear * 1e-6, np.squeeze(attenuation_shear), 'o',
+ markeredgecolor='k', markerfacecolor='None')
+ax4.set_xlim(0, f_max_shear * 1e-6)
+ax4.set_ylim(0, attenuation_th_shear[f_max_shear_index] * 1.1)
+ax4.set_xlabel('Frequency [MHz]')
+ax4.set_ylabel(r'$\alpha$ [dB/cm]')
+
+plt.show()
+
diff --git a/examples/ewp_shear_wave_snells_law/ewp_shear_wave_snells_law.py b/examples/ewp_shear_wave_snells_law/ewp_shear_wave_snells_law.py
new file mode 100644
index 000000000..58e589716
--- /dev/null
+++ b/examples/ewp_shear_wave_snells_law/ewp_shear_wave_snells_law.py
@@ -0,0 +1,256 @@
+import numpy as np
+import matplotlib.pyplot as plt
+from operator import not_
+from copy import deepcopy
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.ksensor import kSensor
+from kwave.kspaceFirstOrder2D import kspace_first_order_2d_gpu
+from kwave.pstdElastic2D import pstd_elastic_2d
+
+from kwave.utils.mapgen import make_arc
+from kwave.utils.matlab import rem
+from kwave.utils.signals import tone_burst
+
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.options.simulation_execution_options import SimulationExecutionOptions
+
+# change scale to 2 to reproduce higher resolution figures in help file
+scale: int = 1
+
+# create the computational grid
+pml_size: int = 10 # [grid points]
+Nx: int = 128 * scale - 2 * pml_size # [grid points]
+Ny: int = 192 * scale - 2 * pml_size # [grid points]
+dx: float = 0.5e-3 / float(scale) # [m]
+dy: float = 0.5e-3 / float(scale) # [m]
+
+kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))
+
+# define the medium properties for the top layer
+cp1 = 1540.0 # compressional wave speed [m/s]
+cs1 = 0.0 # shear wave speed [m/s]
+rho1 = 1000.0 # density [kg/m^3]
+alpha0_p1 = 0.1 # compressional absorption [dB/(MHz^2 cm)]
+alpha0_s1 = 0.1 # shear absorption [dB/(MHz^2 cm)]
+
+# define the medium properties for the bottom layer
+cp2 = 3000.0 # compressional wave speed [m/s]
+cs2 = 1400.0 # shear wave speed [m/s]
+rho2 = 1850.0 # density [kg/m^3]
+alpha0_p2 = 1.0 # compressional absorption [dB/(MHz^2 cm)]
+alpha0_s2 = 1.0 # shear absorption [dB/(MHz^2 cm)]
+
+# create the time array
+cfl: float = 0.1
+t_end: float = 60e-6
+kgrid.makeTime(cp1, cfl, t_end)
+
+# define position of heterogeneous slab
+slab = np.zeros((Nx, Ny), dtype=bool)
+slab[Nx // 2 - 1:, :] = True
+
+# define the source geometry in SI units (where 0, 0 is the grid center)
+arc_pos = [-15e-3, -25e-3] # [m]
+focus_pos = [5e-3, 5e-3] # [m]
+radius = 25e-3 # [m]
+diameter = 20e-3 # [m]
+
+# define the driving signal
+source_freq = 500e3 # [Hz]
+source_strength = 1e6 # [Pa]
+source_cycles = 3 # number of tone burst cycles
+
+# convert the source parameters to grid points
+arc_pos = np.rint(np.asarray(arc_pos) / dx) - 1 + np.asarray([Nx // 2 - 1, Ny // 2 -1]).astype(int)
+focus_pos = np.rint(np.asarray(focus_pos) / dx) - 1 + np.asarray([Nx // 2 - 1, Ny // 2 - 1]).astype(int)
+radius_pos = int(round(radius / dx)) - 1
+diameter_pos = int(round(diameter / dx)) - 1
+
+# force the diameter to be odd
+if (np.isclose(rem(diameter_pos, 2), 0.0) ):
+ diameter_pos = diameter_pos + 1
+
+# generate the source geometry
+source_mask = make_arc(Vector([Nx, Ny]), np.asarray(arc_pos), radius_pos, diameter_pos, Vector(focus_pos))
+
+fs = 1.0 / kgrid.dt
+signal = tone_burst(fs, source_freq, source_cycles, envelope="Gaussian", plot_signal=False,
+ signal_length=0, signal_offset=0)
+
+# =========================================================================
+# FLUID SIMULATION
+# =========================================================================
+
+# assign the medium properties
+sound_speed = cp1 * np.ones((Nx, Ny))
+density = rho1 * np.ones((Nx, Ny))
+alpha_coeff = alpha0_p1 * np.ones((Nx, Ny))
+alpha_power = 2.0
+
+sound_speed[slab] = cp2
+density[slab] = rho2
+alpha_coeff[slab] = alpha0_p2
+
+medium = kWaveMedium(sound_speed,
+ density=density,
+ alpha_coeff=alpha_coeff,
+ alpha_power=alpha_power)
+
+# define the sensor to record the maximum particle velocity everywhere
+sensor = kSensor()
+sensor.mask = np.ones((Nx, Ny), dtype=bool)
+sensor.record = ['u_max_all']
+
+# assign the source
+source = kSource()
+source.p_mask = source_mask
+source.p = source_strength * signal
+
+# set the input settings
+input_filename_p = 'data_p_input.h5'
+output_filename_p = 'data_p_output.h5'
+
+DATA_CAST: str = 'single'
+
+DATA_PATH = '.'
+
+RUN_SIMULATION = True
+
+# options for writing to file, but not doing simulations
+simulation_options = SimulationOptions(data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename_p,
+ output_filename=output_filename_p,
+ save_to_disk_exit=not_(RUN_SIMULATION),
+ data_path=DATA_PATH,
+ pml_inside=False,
+ pml_size=pml_size,
+ hdf_compression_level='lzf')
+
+execution_options = SimulationExecutionOptions(is_gpu_simulation=True, delete_data=False)
+
+# run the fluid simulation
+sensor_data_fluid = kspace_first_order_2d_gpu(medium=deepcopy(medium),
+ kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ simulation_options=deepcopy(simulation_options),
+ execution_options=deepcopy(execution_options))
+
+# =========================================================================
+# ELASTIC SIMULATION
+# =========================================================================
+
+# set the input settings
+input_filename_e = './data_e_input.h5'
+output_filename_e = './data_e_output.h5'
+
+# define the medium properties
+sound_speed_compression = cp1 * np.ones((Nx, Ny))
+sound_speed_shear = cs1 * np.ones((Nx, Ny))
+density = rho1 * np.ones((Nx, Ny))
+alpha_coeff_compression = alpha0_p1 * np.ones((Nx, Ny))
+alpha_coeff_shear = alpha0_s1 * np.ones((Nx, Ny))
+
+sound_speed_compression[slab] = cp2
+sound_speed_shear[slab] = cs2
+density[slab] = rho2
+alpha_coeff_compression[slab] = alpha0_p2
+alpha_coeff_shear[slab] = alpha0_s2
+
+medium_e = kWaveMedium(sound_speed_compression,
+ alpha_coeff=alpha_coeff_compression,
+ alpha_power=2.0,
+ density=density,
+ sound_speed_compression=sound_speed_compression,
+ alpha_coeff_compression=alpha_coeff_compression,
+ sound_speed_shear=sound_speed_shear,
+ alpha_coeff_shear=alpha_coeff_shear)
+
+# assign the source
+source_e = kSource()
+source_e.s_mask = source_mask
+source_e.sxx = -source_strength * signal
+source_e.syy = source_e.sxx
+
+simulation_options_e = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename_e,
+ output_filename=output_filename_e,
+ save_to_disk_exit=not_(RUN_SIMULATION),
+ data_path=DATA_PATH,
+ pml_inside=False,
+ pml_size=pml_size,
+ hdf_compression_level='lzf')
+
+# run the elastic simulation
+sensor_data_elastic = pstd_elastic_2d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source_e),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium_e),
+ simulation_options=deepcopy(simulation_options_e))
+
+
+# =========================================================================
+# VISUALISATION
+# =========================================================================
+
+# define plotting vectors: convert to cm
+x_vec = kgrid.x_vec * 1e3
+y_vec = kgrid.y_vec * 1e3
+
+# calculate square of velocity magnitude for fluid and elastic simulations
+u_f = sensor_data_fluid['ux_max_all']**2 + sensor_data_fluid['uy_max_all']**2
+u_f = u_f[pml_size:-pml_size, pml_size:-pml_size,]
+log_f = 20.0 * np.log10(u_f / np.max(u_f))
+
+u_e = sensor_data_elastic.ux_max_all**2 + sensor_data_elastic.uy_max_all**2
+u_e = np.transpose(u_e)
+log_e = 20.0 * np.log10(u_e / np.max(u_e))
+
+# plot layout of simulation
+fig1, ax1 = plt.subplots(nrows=1, ncols=1)
+_ = ax1.pcolormesh(kgrid.y.T, kgrid.x.T, np.logical_or(slab, source_mask).T,
+ cmap='gray_r', shading='gouraud', alpha=1)
+ax1.invert_yaxis()
+ax1.set_xlabel('y [mm]')
+ax1.set_ylabel('x [mm]')
+
+# plot velocities
+fig2, (ax2a, ax2b) = plt.subplots(nrows=2, ncols=1)
+pcm2a = ax2a.pcolormesh(kgrid.y.T, kgrid.x.T, log_f,
+ shading='gouraud', cmap=plt.colormaps['jet'], clim=(-50.0, 0))
+ax2a.invert_yaxis()
+cb2a = fig2.colorbar(pcm2a, ax=ax2a)
+ax2a.set_xlabel('y [mm]')
+ax2a.set_ylabel('x [mm]')
+cb2a.ax.set_ylabel('[dB]', rotation=90)
+ax2a.set_title('Fluid Model')
+
+pcm2b = ax2b.pcolormesh(kgrid.y.T, kgrid.x.T, log_e,
+ shading='gouraud', cmap=plt.colormaps['jet'], clim=(-50.0, 0))
+ax2b.invert_yaxis()
+cb2b = fig2.colorbar(pcm2b, ax=ax2b)
+ax2b.set_xlabel('y [mm]')
+ax2b.set_ylabel('x [mm]')
+cb2b.ax.set_ylabel('[dB]', rotation=90)
+ax2b.set_title('Elastic Model')
+
+fig3, ax3 = plt.subplots(nrows=1, ncols=1)
+pcm3 = ax3.pcolormesh(kgrid.y.T, kgrid.x.T, u_e,
+ shading='gouraud', cmap=plt.colormaps['jet'])
+ax3.invert_yaxis()
+cb3 = fig3.colorbar(pcm3, ax=ax3)
+ax3.set_xlabel('y [mm]')
+ax3.set_ylabel('x [mm]')
+cb3.ax.set_ylabel('[dB]', rotation=90)
+ax3.set_title('Elastic Model')
+
+plt.show()
\ No newline at end of file
diff --git a/examples/ivp_1d_simulation.py b/examples/ivp_1d_simulation.py
new file mode 100644
index 000000000..e0d44340b
--- /dev/null
+++ b/examples/ivp_1d_simulation.py
@@ -0,0 +1,94 @@
+# Simulations In One Dimension Example
+#
+# This example provides a simple demonstration of using k-Wave for the
+# simulation and detection of the pressure field generated by an initial
+# pressure distribution within a one-dimensional heterogeneous propagation
+# medium. It builds on the Homogeneous Propagation Medium and Heterogeneous
+# Propagation Medium examples.
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+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.kspaceFirstOrder1D import kspace_first_order_1D
+
+from kwave.options.simulation_options import SimulationOptions
+
+
+# =========================================================================
+# SIMULATION
+# =========================================================================
+
+# create the computational grid
+Nx: int = 512 # number of grid points in the x (row) direction
+dx: float = 0.05e-3 # grid point spacing in the x direction [m]
+
+grid_size_points = Vector([Nx, ])
+grid_spacing_meters = Vector([dx, ])
+
+# create the k-space grid
+kgrid = kWaveGrid(grid_size_points, grid_spacing_meters)
+
+# define the properties of the propagation medium
+sound_speed = 1500.0 * np.ones((Nx, 1)) # [m/s]
+sound_speed[:np.round(Nx / 3).astype(int) - 1] = 2000.0 # [m/s]
+density = 1000.0 * np.ones((Nx, 1)) # [kg/m^3]
+density[np.round(4 * Nx / 5).astype(int) - 1:] = 1500.0 # [kg/m^3]
+medium = kWaveMedium(sound_speed=sound_speed, density=density)
+
+# Create the source object
+source = kSource()
+
+# create initial pressure distribution using a smoothly shaped sinusoid
+x_pos: int = 280 # [grid points]
+width: int = 100 # [grid points]
+height: int = 1 # [au]
+p0 = np.linspace(0.0, 2.0 * np.pi, width + 1)
+
+part1 = np.zeros(x_pos).astype(float)
+part2 = (height / 2.0) * np.sin(p0 - np.pi / 2.0) + (height / 2.0)
+part3 = np.zeros(Nx - x_pos - width - 1).astype(float)
+source.p0 = np.concatenate([part1, part2, part3])
+
+# create a Cartesian sensor mask recording the pressure
+sensor = kSensor()
+sensor.record = ["p"]
+
+# this hack is needed to ensure that the sensor is in [1,2] dimensions
+mask = np.array([-10e-3, 10e-3]) # [mm]
+mask = mask[:, np.newaxis].T
+sensor.mask = mask
+
+# set the simulation time to capture the reflections
+c_max = np.max(medium.sound_speed.flatten()) # [m/s]
+t_end = 2.5 * kgrid.x_size / c_max # [s]
+
+# define the time array
+kgrid.makeTime(c_max, t_end=t_end)
+
+# define the simulation options
+simulation_options = SimulationOptions(data_cast="off", save_to_disk=False)
+
+# run the simulation
+sensor_data = kspace_first_order_1D(kgrid, source, sensor, medium, simulation_options=simulation_options)
+
+# =========================================================================
+# VISUALISATION
+# =========================================================================
+
+# plot the recorded time signals
+_, ax1 = plt.subplots()
+ax1.plot(sensor_data['p'][0, :], 'b-')
+ax1.plot(sensor_data['p'][1, :], 'r-')
+ax1.grid(True)
+ax1.set_ylim(-0.1, 0.7)
+ax1.set_ylabel('Pressure')
+ax1.set_xlabel('Time Step')
+ax1.legend(['Sensor Position 1', 'Sensor Position 2'])
+plt.show()
diff --git a/kwave/kWaveSimulation.py b/kwave/kWaveSimulation.py
index f4f7d9c71..bf8eb80f3 100644
--- a/kwave/kWaveSimulation.py
+++ b/kwave/kWaveSimulation.py
@@ -2,6 +2,7 @@
from dataclasses import dataclass
import numpy as np
+import copy
from kwave.data import Vector
from kwave.kWaveSimulation_helper import (
@@ -10,6 +11,7 @@
expand_grid_matrices,
create_absorption_variables,
scale_source_terms_func,
+ create_storage_variables
)
from kwave.kgrid import kWaveGrid
from kwave.kmedium import kWaveMedium
@@ -31,7 +33,8 @@
@dataclass
class kWaveSimulation(object):
def __init__(
- self, kgrid: kWaveGrid, source: kSource, sensor: NotATransducer, medium: kWaveMedium, simulation_options: SimulationOptions
+ self, kgrid: kWaveGrid, source: kSource, sensor: NotATransducer,
+ medium: kWaveMedium, simulation_options: SimulationOptions
):
self.precision = None
self.kgrid = kgrid
@@ -40,6 +43,8 @@ def __init__(
self.sensor = sensor
self.options = simulation_options
+ self.sensor_data = None
+
# =========================================================================
# FLAGS WHICH DEPEND ON USER INPUTS (THESE SHOULD NOT BE MODIFIED)
# =========================================================================
@@ -48,15 +53,18 @@ def __init__(
# check if performing time reversal, and replace inputs to explicitly use a
# source with a dirichlet boundary condition
- if self.sensor.time_reversal_boundary_data is not None:
- # define a new source structure
- source = {"p_mask": self.sensor.p_mask, "p": np.flip(self.sensor.time_reversal_boundary_data, 2), "p_mode": "dirichlet"}
+ if hasattr(self.sensor, 'time_reversal_boundary_data') and self.sensor.time_reversal_boundary_data is not None:
+ # define a _new_ source structure
+ source = {"p_mask": self.sensor.p_mask,
+ "p": np.flip(self.sensor.time_reversal_boundary_data, 2),
+ "p_mode": "dirichlet"}
- # define a new sensor structure
+ # define a _new_ sensor structure
Nx, Ny, Nz = self.kgrid.Nx, self.kgrid.Ny, self.kgrid.Nz
sensor = kSensor(mask=np.ones((Nx, Ny, max(1, Nz))), record=["p_final"])
# set time reversal flag
self.userarg_time_rev = True
+
else:
# set time reversal flag
self.userarg_time_rev = False
@@ -70,15 +78,17 @@ def __init__(
self.binary_sensor_mask = True
# check if the sensor mask is defined as a list of cuboid corners
- if self.sensor.mask is not None and self.sensor.mask.shape[0] == (2 * self.kgrid.dim):
+ if self.sensor.mask is not None and np.shape(self.sensor.mask)[0] == (2 * self.kgrid.dim):
self.userarg_cuboid_corners = True
else:
self.userarg_cuboid_corners = False
- #: If tse sensor is an object of the kWaveTransducer class
+ #: If the sensor is an object of the kWaveTransducer class
self.transducer_sensor = False
+ # set the recorder
self.record = Recorder()
+ # print("Not time-reversal Recorder:", self.record, self.record.p)
# transducer source flags
#: transducer is object of kWaveTransducer class
@@ -101,7 +111,7 @@ def __init__(
# filenames
self.STREAM_TO_DISK_FILENAME = "temp_sensor_data.bin" #: default disk stream filename
- self.LOG_NAME = ["k-Wave-Log-", get_date_string()] #: default log filename
+ self.LOG_NAME = ["k-Wave-Log-", get_date_string()] #: default log filename
self.calling_func_name = None
logging.log(logging.INFO, f" start time: {get_date_string()}")
@@ -129,6 +139,19 @@ def __init__(
self.c0 = None #: Alias to medium.sound_speed
self.index_data_type = None
+ self.num_recorded_time_points = None
+
+ self.record_u_split_field: bool = False
+
+ @property
+ def is_nonlinear(self):
+ """
+ Returns:
+ Set simulation to nonlinear if medium is nonlinear.
+ """
+ return self.medium.is_nonlinear()
+
+
@property
def equation_of_state(self):
"""
@@ -141,14 +164,13 @@ def equation_of_state(self):
else:
return "absorbing"
else:
- return "loseless"
+ return "lossless"
@property
def use_sensor(self):
"""
Returns:
False if no output of any kind is required
-
"""
return self.sensor is not None
@@ -157,8 +179,8 @@ def blank_sensor(self):
"""
Returns
True if sensor.mask is not defined but _max_all or _final variables are still recorded
-
"""
+
fields = ["p", "p_max", "p_min", "p_rms", "u", "u_non_staggered", "u_split_field", "u_max", "u_min", "u_rms", "I", "I_avg"]
if not (isinstance(self.sensor, NotATransducer) or any(self.record.is_set(fields)) or self.time_rev):
return True
@@ -169,8 +191,12 @@ def kelvin_voigt_model(self):
"""
Returns:
Whether the simulation is elastic with absorption
-
"""
+
+ is_elastic_simulation = self.options.simulation_type.is_elastic_simulation()
+ if is_elastic_simulation:
+ if ((self.medium.alpha_coeff_compression is not None) and (self.medium.alpha_coeff_shear is not None)):
+ return True
return False
@property
@@ -178,7 +204,6 @@ def nonuniform_grid(self):
"""
Returns:
True if the computational grid is non-uniform
-
"""
return self.kgrid.nonuniform
@@ -187,7 +212,6 @@ def time_rev(self):
"""
Returns:
True for time reversal simulaions using sensor.time_reversal_boundary_data
-
"""
if self.sensor is not None and not isinstance(self.sensor, NotATransducer):
if not self.options.simulation_type.is_elastic_simulation() and self.sensor.time_reversal_boundary_data is not None:
@@ -200,7 +224,6 @@ def elastic_time_rev(self):
"""
Returns:
True if using time reversal with the elastic code
-
"""
return False
@@ -209,7 +232,6 @@ def compute_directivity(self):
"""
Returns:
True if directivity calculations in 2D are used by setting sensor.directivity_angle
-
"""
if self.sensor is not None and not isinstance(self.sensor, NotATransducer):
if self.kgrid.dim == 2:
@@ -226,16 +248,19 @@ def cuboid_corners(self):
Whether the sensor.mask is a list of cuboid corners
"""
if self.sensor is not None and not isinstance(self.sensor, NotATransducer):
- if not self.blank_sensor and self.sensor.mask.shape[0] == 2 * self.kgrid.dim:
- return True
+ if self.sensor.mask is not None:
+ if not self.blank_sensor and np.shape(np.asarray(self.sensor.mask))[0] == 2 * self.kgrid.dim:
+ return True
return self.userarg_cuboid_corners
##############
# flags which control the types of source used
##############
+
@property
- def source_p0(self): # initial pressure
+ def source_p0(self):
"""
+ initial pressure
Returns:
Whether initial pressure source is present (default=False)
@@ -247,14 +272,20 @@ def source_p0(self): # initial pressure
return flag
@property
- def source_p0_elastic(self): # initial pressure in the elastic code
+ def source_p0_elastic(self):
"""
+ initial pressure in the elastic code
+
Returns:
Whether initial pressure source is present in the elastic code (default=False)
"""
- # Not clear where this flag is set
- return False
+ flag: bool = False
+ if not isinstance(self.source, NotATransducer) and self.source.p0 is not None \
+ and self.options.simulation_type.is_elastic_simulation():
+ # set flag
+ flag = True
+ return flag
@property
def source_p(self):
@@ -271,8 +302,10 @@ def source_p(self):
return flag
@property
- def source_p_labelled(self): # time-varying pressure with labelled source mask
+ def source_p_labelled(self):
"""
+ time-varying pressure with labelled source mask
+
Returns:
True/False if labelled/binary source mask, respectively.
@@ -285,7 +318,7 @@ def source_p_labelled(self): # time-varying pressure with labelled source mask
return flag
@property
- def source_ux(self) -> bool:
+ def source_ux(self):
"""
Returns:
Whether time-varying particle velocity source is used in X-direction
@@ -299,7 +332,7 @@ def source_ux(self) -> bool:
return flag
@property
- def source_uy(self) -> bool:
+ def source_uy(self):
"""
Returns:
Whether time-varying particle velocity source is used in Y-direction
@@ -313,7 +346,7 @@ def source_uy(self) -> bool:
return flag
@property
- def source_uz(self) -> bool:
+ def source_uz(self):
"""
Returns:
Whether time-varying particle velocity source is used in Z-direction
@@ -353,7 +386,7 @@ def source_sxx(self):
"""
flag = False
if not isinstance(self.source, NotATransducer) and self.source.sxx is not None:
- flag = len(self.source.sxx[0])
+ flag = np.shape(self.source.sxx)[1]
return flag
@property
@@ -365,7 +398,7 @@ def source_syy(self):
"""
flag = False
if not isinstance(self.source, NotATransducer) and self.source.syy is not None:
- flag = len(self.source.syy[0])
+ flag = np.shape(self.source.syy)[1]
return flag
@property
@@ -377,7 +410,7 @@ def source_szz(self):
"""
flag = False
if not isinstance(self.source, NotATransducer) and self.source.szz is not None:
- flag = len(self.source.szz[0])
+ flag = np.shape(self.source.szz)[1]
return flag
@property
@@ -389,7 +422,7 @@ def source_sxy(self):
"""
flag = False
if not isinstance(self.source, NotATransducer) and self.source.sxy is not None:
- flag = len(self.source.sxy[0])
+ flag = np.shape(self.source.sxy)[1]
return flag
@property
@@ -401,7 +434,7 @@ def source_sxz(self):
"""
flag = False
if not isinstance(self.source, NotATransducer) and self.source.sxz is not None:
- flag = len(self.source.sxz[0])
+ flag = np.shape(self.source.sxz)[1]
return flag
@property
@@ -413,7 +446,7 @@ def source_syz(self):
"""
flag = False
if not isinstance(self.source, NotATransducer) and self.source.syz is not None:
- flag = len(self.source.syz[0])
+ flag = np.shape(self.source.syz)[1]
return flag
@property
@@ -460,6 +493,7 @@ def use_w_source_correction_u(self):
flag = True
return flag
+
def input_checking(self, calling_func_name) -> None:
"""
Check the input fields for correctness and validness
@@ -476,8 +510,17 @@ def input_checking(self, calling_func_name) -> None:
self.check_calling_func_name_and_dim(calling_func_name, k_dim)
- # run subscript to check optional inputs
+ # check optional inputs
self.options = SimulationOptions.option_factory(self.kgrid, self.options)
+
+ # add options which are properties of the class
+ self.options.use_sensor = self.use_sensor
+ self.options.kelvin_voigt_model = self.kelvin_voigt_model
+ self.options.blank_sensor = self.blank_sensor
+ self.options.cuboid_corners = self.cuboid_corners # there is the userarg_ values as well
+ self.options.nonuniform_grid = self.nonuniform_grid
+ self.options.elastic_time_rev = self.elastic_time_rev
+
opt = self.options
# TODO(Walter): clean this up with getters in simulation options pml size
@@ -503,17 +546,95 @@ def input_checking(self, calling_func_name) -> None:
display_simulation_params(self.kgrid, self.medium, is_elastic_code)
self.smooth_and_enlarge(self.source, k_dim, Vector(self.kgrid.N), opt)
+
self.create_sensor_variables()
+
self.create_absorption_vars()
+
self.assign_pseudonyms(self.medium, self.kgrid)
+
self.scale_source_terms(opt.scale_source_terms)
- self.create_pml_indices(
- kgrid_dim=self.kgrid.dim,
- kgrid_N=Vector(self.kgrid.N),
- pml_size=pml_size,
- pml_inside=opt.pml_inside,
- is_axisymmetric=opt.simulation_type.is_axisymmetric(),
- )
+
+ # move all this inside create_storage_variables?
+ # a copy of record is passed through, and use to update the
+ if is_elastic_code:
+ record_old = copy.deepcopy(self.record)
+ if not self.blank_sensor:
+ sensor_x = self.sensor.mask[0, :]
+ else:
+ sensor_x = None
+
+ values = dotdict({"sensor_x": sensor_x,
+ "sensor_mask_index": self.sensor_mask_index,
+ "record": record_old,
+ "sensor_data_buffer_size": self.s_source_pos_index})
+
+ if self.record.u_split_field:
+ self.record_u_split_field = self.record.u_split_field
+
+ flags = dotdict({"use_sensor": self.use_sensor,
+ "blank_sensor": self.blank_sensor,
+ "binary_sensor_mask": self.binary_sensor_mask,
+ "record_u_split_field": self.record.u_split_field,
+ "time_rev": self.time_rev,
+ "reorder_data": self.reorder_data,
+ "transducer_receive_elevation_focus": self.transducer_receive_elevation_focus,
+ "axisymmetric": opt.simulation_type.is_axisymmetric(),
+ "transducer_sensor": self.transducer_sensor,
+ "use_cuboid_corners": self.cuboid_corners})
+
+ # this creates the storage variables by determining the spatial locations of the data which is in record.
+ flags, self.record, self.sensor_data, self.num_recorded_time_points = create_storage_variables(self.kgrid,
+ self.sensor,
+ opt,
+ values,
+ flags,
+ self.record)
+ else:
+ record_old = copy.deepcopy(self.record)
+ if not self.blank_sensor:
+ if k_dim == 1:
+ # this has been declared in check_sensor
+ sensor_x = self.sensor_x
+ else:
+ sensor_x = self.sensor.mask[0, :]
+ else:
+ sensor_x = None
+
+
+ values = dotdict({"sensor_x": sensor_x,
+ "sensor_mask_index": self.sensor_mask_index,
+ "record": record_old,
+ "sensor_data_buffer_size": self.s_source_pos_index})
+
+ if self.record.u_split_field:
+ self.record_u_split_field = self.record.u_split_field
+
+ flags = dotdict({"use_sensor": self.use_sensor,
+ "blank_sensor": self.blank_sensor,
+ "binary_sensor_mask": self.binary_sensor_mask,
+ "record_u_split_field": self.record.u_split_field,
+ "time_rev": self.time_rev,
+ "reorder_data": self.reorder_data,
+ "transducer_receive_elevation_focus": self.transducer_receive_elevation_focus,
+ "axisymmetric": opt.simulation_type.is_axisymmetric(),
+ "transducer_sensor": self.transducer_sensor,
+ "use_cuboid_corners": self.cuboid_corners})
+
+ # this creates the storage variables by determining the spatial locations of the data which is in record.
+ flags, self.record, self.sensor_data, self.num_recorded_time_points = create_storage_variables(self.kgrid,
+ self.sensor,
+ opt,
+ values,
+ flags,
+ self.record)
+
+ self.create_pml_indices(kgrid_dim=self.kgrid.dim,
+ kgrid_N=Vector(self.kgrid.N),
+ pml_size=pml_size,
+ pml_inside=opt.pml_inside,
+ is_axisymmetric=opt.simulation_type.is_axisymmetric())
+
@staticmethod
def check_calling_func_name_and_dim(calling_func_name, kgrid_dim) -> None:
@@ -527,7 +648,6 @@ def check_calling_func_name_and_dim(calling_func_name, kgrid_dim) -> None:
Returns:
None
"""
- assert not calling_func_name.startswith(("pstdElastic", "kspaceElastic")), "Elastic simulation is not supported."
if calling_func_name == "kspaceFirstOrder1D":
assert kgrid_dim == 1, f"kgrid has the wrong dimensionality for {calling_func_name}."
@@ -536,6 +656,10 @@ def check_calling_func_name_and_dim(calling_func_name, kgrid_dim) -> None:
elif calling_func_name in ["kspaceFirstOrder3D", "pstdElastic3D", "kspaceElastic3D"]:
assert kgrid_dim == 3, f"kgrid has the wrong dimensionality for {calling_func_name}."
+ elif calling_func_name in ["pstd_elastic_3d", "pstd_elastic_3d_gpu"]:
+ assert kgrid_dim == 3, f"kgrid has the wrong dimensionality for {calling_func_name}."
+
+
@staticmethod
def print_start_status(is_elastic_code: bool) -> None:
"""
@@ -547,12 +671,13 @@ def print_start_status(is_elastic_code: bool) -> None:
Returns:
None
"""
- if is_elastic_code: # pragma: no cover
- logging.log(logging.INFO, "Running k-Wave elastic simulation...")
+ if is_elastic_code:
+ logging.log(logging.INFO, "Running k-Wave elastic simulation ...")
else:
- logging.log(logging.INFO, "Running k-Wave simulation...")
+ logging.log(logging.INFO, "Running k-Wave acoustic simulation ...")
logging.log(logging.INFO, f" start time: {get_date_string()}")
+
def set_index_data_type(self) -> None:
"""
Pre-calculate the data type needed to store the matrix indices given the
@@ -564,6 +689,7 @@ def set_index_data_type(self) -> None:
total_grid_points = self.kgrid.total_grid_points
self.index_data_type = get_smallest_possible_type(total_grid_points, "uint", default="double")
+
@staticmethod
def check_medium(medium, kgrid_k, simulation_type: SimulationType) -> bool:
"""
@@ -596,9 +722,10 @@ def check_medium(medium, kgrid_k, simulation_type: SimulationType) -> bool:
medium.check_fields(kgrid_k.shape)
return user_medium_density_input
+
def check_sensor(self, kgrid_dim) -> None:
"""
- Check the Sensor properties for correctness and validity
+ Check the sensor properties for correctness and validity
Args:
k_dim: kWaveGrid dimensionality
@@ -606,11 +733,14 @@ def check_sensor(self, kgrid_dim) -> None:
Returns:
None
"""
+
# =========================================================================
# CHECK SENSOR STRUCTURE INPUTS
# =========================================================================
+
# check sensor fields
if self.sensor is not None:
+
# check the sensor input is valid
# TODO FARID move this check as a type checking
assert isinstance(
@@ -619,7 +749,9 @@ def check_sensor(self, kgrid_dim) -> None:
# check if sensor is a transducer, otherwise check input fields
if not isinstance(self.sensor, NotATransducer):
+
if kgrid_dim == 2:
+
# check for sensor directivity input and set flag
directivity = self.sensor.directivity
if directivity is not None and self.sensor.directivity.angle is not None:
@@ -628,7 +760,7 @@ def check_sensor(self, kgrid_dim) -> None:
# check sensor.directivity.pattern and sensor.mask have the same size
assert (
- directivity.angle.shape == self.sensor.mask.shape
+ directivity.angle.shape == np.shape(self.sensor.mask)
), "sensor.directivity.angle and sensor.mask must be the same size."
# check if directivity size input exists, otherwise make it
@@ -644,8 +776,9 @@ def check_sensor(self, kgrid_dim) -> None:
# check for time reversal inputs and set flags
if not self.options.simulation_type.is_elastic_simulation() and self.sensor.time_reversal_boundary_data is not None:
self.record.p = False
+ print("don't think time reversal is implemented")
- # check for sensor.record and set usage flgs - if no flgs are
+ # check for sensor.record and set usage flgs - if no flags are
# given, the time history of the acoustic pressure is recorded by
# default
if self.sensor.record is not None:
@@ -663,14 +796,16 @@ def check_sensor(self, kgrid_dim) -> None:
# and _final variables
fields = ["p", "p_max", "p_min", "p_rms", "u", "u_non_staggered", "u_split_field", "u_max", "u_min", "u_rms", "I", "I_avg"]
if any(self.record.is_set(fields)):
- assert self.sensor.mask is not None
+ assert self.sensor.mask is not None, "sensor.mask should be set"
# check if sensor mask is a binary grid, a set of cuboid corners,
# or a set of Cartesian interpolation points
if not self.blank_sensor:
+
+ # binary grid
if (kgrid_dim == 3 and num_dim2(self.sensor.mask) == 3) or (
- kgrid_dim != 3 and (self.sensor.mask.shape == self.kgrid.k.shape)
- ):
+ kgrid_dim != 3 and (np.shape(self.sensor.mask) == self.kgrid.k.shape)):
+
# check the grid is binary
assert self.sensor.mask.sum() == (
self.sensor.mask.size - (self.sensor.mask == 0).sum()
@@ -679,53 +814,58 @@ def check_sensor(self, kgrid_dim) -> None:
# check the grid is not empty
assert self.sensor.mask.sum() != 0, "sensor.mask must be a binary grid with at least one element set to 1."
- elif self.sensor.mask.shape[0] == 2 * kgrid_dim:
+ # cuboid corners
+ elif np.shape(self.sensor.mask)[0] == 2 * kgrid_dim:
+
+ # set cuboid_corners flag?
+
# make sure the points are integers
- assert np.all(self.sensor.mask % 1 == 0), "sensor.mask cuboid corner indices must be integers."
+ assert np.all(np.asarray(self.sensor.mask) % 1 == 0), "sensor.mask cuboid corner indices must be integers."
# store a copy of the cuboid corners
self.record.cuboid_corners_list = self.sensor.mask
# check the list makes sense
- if np.any(self.sensor.mask[self.kgrid.dim :, :] - self.sensor.mask[: self.kgrid.dim, :] < 0):
+ if np.any(np.asarray(self.sensor.mask)[self.kgrid.dim:, :] - np.asarray(self.sensor.mask)[:self.kgrid.dim, :] < 0):
if kgrid_dim == 1:
raise ValueError("sensor.mask cuboid corners must be defined " "as [x1, x2; ...]." " where x2 => x1, etc.")
elif kgrid_dim == 2:
raise ValueError(
- "sensor.mask cuboid corners must be defined " "as [x1, y1, x2, y2; ...]." " where x2 => x1, etc."
+ "sensor.mask cuboid corners must be defined " "as [[x1, y1, x2, y2], [... ] ...]." " where x2 => x1, etc."
)
elif kgrid_dim == 3:
raise ValueError(
"sensor.mask cuboid corners must be defined"
- " as [x1, y1, z1, x2, y2, z2; ...]."
+ " as [[x1, y1, z1, x2, y2, z2], [...], ...]."
" where x2 => x1, etc."
)
# check the list are within bounds
- if np.any(self.sensor.mask < 1):
+ if np.any(np.asarray(self.sensor.mask) < 0):
raise ValueError("sensor.mask cuboid corners must be within the grid.")
else:
if kgrid_dim == 1:
- if np.any(self.sensor.mask > self.kgrid.Nx):
+ if np.any(np.asarray(self.sensor.mask) > self.kgrid.Nx - 1):
raise ValueError("sensor.mask cuboid corners must be within the grid.")
elif kgrid_dim == 2:
- if np.any(self.sensor.mask[[0, 2], :] > self.kgrid.Nx) or np.any(
- self.sensor.mask[[1, 3], :] > self.kgrid.Ny
+ if np.any(np.asarray(self.sensor.mask)[[0, 2], :] > self.kgrid.Nx - 1) or np.any(
+ np.asarray(self.sensor.mask)[[1, 3], :] > self.kgrid.Ny -1
):
raise ValueError("sensor.mask cuboid corners must be within the grid.")
elif kgrid_dim == 3:
+ mask = np.asarray(self.sensor.mask)
if (
- np.any(self.sensor.mask[[0, 3], :] > self.kgrid.Nx)
- or np.any(self.sensor.mask[[1, 4], :] > self.kgrid.Ny)
- or np.any(self.sensor.mask[[2, 5], :] > self.kgrid.Nz)
+ np.any(mask[[0, 3], :] > self.kgrid.Nx - 1)
+ or np.any(mask[[1, 4], :] > self.kgrid.Ny - 1)
+ or np.any(mask[[2, 5], :] > self.kgrid.Nz - 1)
):
raise ValueError("sensor.mask cuboid corners must be within the grid.")
# create a binary mask for display from the list of corners
# TODO FARID mask should be option_factory in sensor not here
self.sensor.mask = np.zeros_like(self.kgrid.k, dtype=bool)
- cuboid_corners_list = self.record.cuboid_corners_list
- for cuboid_index in range(cuboid_corners_list.shape[1]):
+ cuboid_corners_list = np.asarray(self.record.cuboid_corners_list)
+ for cuboid_index in range(np.shape(cuboid_corners_list)[1]):
if self.kgrid.dim == 1:
self.sensor.mask[cuboid_corners_list[0, cuboid_index] : cuboid_corners_list[1, cuboid_index]] = 1
if self.kgrid.dim == 2:
@@ -739,33 +879,45 @@ def check_sensor(self, kgrid_dim) -> None:
cuboid_corners_list[1, cuboid_index] : cuboid_corners_list[4, cuboid_index],
cuboid_corners_list[2, cuboid_index] : cuboid_corners_list[5, cuboid_index],
] = 1
+
+ # cartesian sensor
else:
+
# check the Cartesian sensor mask is the correct size
# (1 x N, 2 x N, 3 x N)
assert (
- self.sensor.mask.shape[0] == kgrid_dim and num_dim2(self.sensor.mask) <= 2
+ np.shape(self.sensor.mask)[0] == kgrid_dim and num_dim2(self.sensor.mask) <= 2
), f"Cartesian sensor.mask for a {kgrid_dim}D simulation must be given as a {kgrid_dim} by N array."
# set Cartesian mask flag (this is modified in
- # createStorageVariables if the interpolation setting is
+ # create_storage_variables if the interpolation setting is
# set to nearest)
self.binary_sensor_mask = False
+ # print("here!")
+
# extract Cartesian data from sensor mask
if kgrid_dim == 1:
+
# align sensor data as a column vector to be the
# same as kgrid.x_vec so that calls to interp1
# return data in the correct dimension
- self.sensor_x = np.reshape((self.sensor.mask, (-1, 1)))
+
+ # print(self.sensor.mask.shape, self.kgrid.x_vec.shape)
+ self.sensor_x = np.reshape(self.sensor.mask, (-1, 1))
+
+ # print(self.sensor.mask.shape, self.kgrid.x_vec.shape)
+
+ print("############## self.record.sensor_x = self.sensor_x", self.sensor_x)
# add sensor_x to the record structure for use with
- # the _extractSensorData subfunction
+ # the extract_sensor_data method
self.record.sensor_x = self.sensor_x
- "record.sensor_x = sensor_x;"
elif kgrid_dim == 2:
self.sensor_x = self.sensor.mask[0, :]
self.sensor_y = self.sensor.mask[1, :]
+
elif kgrid_dim == 3:
self.sensor_x = self.sensor.mask[0, :]
self.sensor_y = self.sensor.mask[1, :]
@@ -788,15 +940,18 @@ def check_sensor(self, kgrid_dim) -> None:
# append the reordering data
new_col_pos = length(sensor.time_reversal_boundary_data(1, :)) + 1;
sensor.time_reversal_boundary_data(:, new_col_pos) = order_index;
-
+
# reorder p0 based on the order_index
sensor.time_reversal_boundary_data = sort_rows(sensor.time_reversal_boundary_data, new_col_pos);
-
+
# remove the reordering data
sensor.time_reversal_boundary_data = sensor.time_reversal_boundary_data(:, 1:new_col_pos - 1);
"""
+ else:
+ # print('is a blank sensor')
+ pass
else:
- # set transducer sensor flag
+ # set transducer_sensor flag to true, i.e. the sensor is a transducer
self.transducer_sensor = True
self.record.p = False
@@ -813,6 +968,7 @@ def check_sensor(self, kgrid_dim) -> None:
if kgrid_dim == 2 and self.use_sensor and self.compute_directivity and self.time_rev:
logging.log(logging.WARN, "sensor directivity fields are not used for time reversal.")
+
def check_source(self, k_dim, k_Nt) -> None:
"""
Check the source properties for correctness and validity
@@ -841,10 +997,10 @@ def check_source(self, k_dim, k_Nt) -> None:
"""
check allowable source types
-
- Depending on the kgrid dimensionality and the simulation type,
+
+ Depending on the kgrid dimensionality and the simulation type,
following fields are allowed & might be use:
-
+
kgrid.dim == 1:
non-elastic code:
['p0', 'p', 'p_mask', 'p_mode', 'p_frequency_ref', 'ux', 'u_mask', 'u_mode', 'u_frequency_ref']
@@ -862,8 +1018,49 @@ def check_source(self, k_dim, k_Nt) -> None:
self.source.validate(self.kgrid)
+ # check for initial pressure input
+ if self.source.p0 is not None:
+
+ # check size and contents
+ if np.allclose(np.abs(self.source.p0), np.zeros_like(self.source.p0)):
+ # if the initial pressure is empty or zero, remove field
+ del self.source.p0
+ raise RuntimeWarning('All entries in source.p0 are close to zero')
+
+ if np.any(np.size(np.squeeze(self.source.p0)) != np.size(np.squeeze(self.kgrid.k))):
+ # throw an error if p0 is not the correct size
+ raise RuntimeError('source.p0 must be the same size as the computational grid')
+
+ # if using the elastic code, reformulate source.p0 in terms of the
+ # stress source terms using the fact that source.p = [0.5 0.5] /
+ # (2*CFL) is the same as source.p0 = 1
+ if self.options.simulation_type.is_elastic_simulation():
+
+ print('DEFINE AS A STRESS SOURCE')
+
+ self.source.s_mask = np.ones(np.shape(self.kgrid.k), dtype=bool)
+
+ if self.options.smooth_p0:
+ # print('smooth p0')
+ self.source.p0 = smooth(self.source.p0, restore_max=True)
+
+ self.source.sxx = np.empty((np.size(self.source.p0), 2))
+ self.source.sxx[:, 0] = -self.source.p0.flatten(order="F") / 2.0
+ self.source.sxx[:, 1] = -self.source.p0.flatten(order="F") / 2.0
+
+ self.source.syy = copy.deepcopy(self.source.sxx)
+
+ self.s_source_pos_index = matlab_find(self.source.s_mask)
+ self.s_source_sig_index = self.s_source_pos_index
+
+ if self.kgrid.dim == 3:
+ self.source.szz = copy.deepcopy(self.source.sxx)
+
+
+
# check for a time varying pressure source input
if self.source.p is not None:
+
# check the source mode input is valid
if self.source.p_mode is None:
self.source.p_mode = self.SOURCE_P_MODE_DEF
@@ -871,7 +1068,8 @@ def check_source(self, k_dim, k_Nt) -> None:
if self.source_p > k_Nt:
logging.log(logging.WARN, " source.p has more time points than kgrid.Nt, remaining time points will not be used.")
- # create an indexing variable corresponding to the location of all the source elements
+ # create an indexing variable corresponding to the location of all the source elements.
+ # matlab_find is matlab indexed
self.p_source_pos_index = matlab_find(self.source.p_mask)
# check if the mask is binary or labelled
@@ -880,67 +1078,130 @@ def check_source(self, k_dim, k_Nt) -> None:
# create a second indexing variable
if p_unique.size <= 2 and p_unique.sum() == 1:
# set signal index to all elements
- self.p_source_sig_index = ":"
+ self.p_source_sig_index = np.arange(0, np.shape(self.source.p)[0]) + 1
else:
# set signal index to the labels (this allows one input signal
# to be used for each source label)
- self.p_source_sig_index = self.source.p_mask(self.source.p_mask != 0)
+ self.p_source_sig_index = self.source.p_mask[self.source.p_mask != 0] + int(1)
# convert the data type depending on the number of indices
self.p_source_pos_index = cast_to_type(self.p_source_pos_index, self.index_data_type)
+
if self.source_p_labelled:
self.p_source_sig_index = cast_to_type(self.p_source_sig_index, self.index_data_type)
+
# check for time varying velocity source input and set source flag
if any([(getattr(self.source, k) is not None) for k in ["ux", "uy", "uz", "u_mask"]]):
+
# check the source mode input is valid
if self.source.u_mode is None:
self.source.u_mode = self.SOURCE_U_MODE_DEF
# create an indexing variable corresponding to the location of all
- # the source elements
+ # the source elements. The domain has not yet been enlarged. minus one to get python indexing
self.u_source_pos_index = matlab_find(self.source.u_mask)
# check if the mask is binary or labelled
u_unique = np.unique(self.source.u_mask)
- # create a second indexing variable
+ # create a second indexing variable. This is u_source_sig_index, the signal index.
+ # If binary.
if u_unique.size <= 2 and u_unique.sum() == 1:
# set signal index to all elements
- self.u_source_sig_index = ":"
+ if self.source.ux is not None and self.source.uy is not None and np.shape(self.source.ux) != np.shape(self.source.uy):
+ raise RuntimeError('Sizes are wrong')
+ if self.source.ux is not None:
+ self.u_source_sig_index = np.arange(0, np.shape(self.source.ux)[0]) + 1
+ elif self.source.uy is not None:
+ self.u_source_sig_index = np.arange(0, np.shape(self.source.uy)[0]) + 1
+ elif self.source.uz is not None:
+ self.u_source_sig_index = np.arange(0, np.shape(self.source.uz)[0]) + 1
+
else:
# set signal index to the labels (this allows one input signal
# to be used for each source label)
- self.u_source_sig_index = self.source.u_mask[self.source.u_mask != 0]
+
+ # self.u_source_sig_index = self.source.u_mask[self.source.u_mask != 0] + 1
+
+ arr = np.where(self.source.u_mask.flatten(order="F") != 0)[0]
+ self.u_source_sig_index = self.source.u_mask.flatten(order="F")[arr]
+
# convert the data type depending on the number of indices
self.u_source_pos_index = cast_to_type(self.u_source_pos_index, self.index_data_type)
+
if self.source_u_labelled:
self.u_source_sig_index = cast_to_type(self.u_source_sig_index, self.index_data_type)
+
# check for time varying stress source input and set source flag
- if any([(getattr(self.source, k) is not None) for k in ["sxx", "syy", "szz", "sxy", "sxz", "syz", "s_mask"]]):
- # create an indexing variable corresponding to the location of all
- # the source elements
- raise NotImplementedError
- "s_source_pos_index = find(source.s_mask != 0);"
+ if any([(getattr(self.source, k) is not None) for k in ["sxx", "syy", "szz", "sxy", "sxz", "syz", "s_mask"]]) and not self.source_p0_elastic:
+
+ # check the source mode input is valid
+ if self.source.s_mode is None:
+ self.source.s_mode = self.SOURCE_S_MODE_DEF
+
+ # create an indexing variable corresponding to the location of all the source elements
+ self.s_source_pos_index = matlab_find(self.source.s_mask) #np.where(self.source.s_mask != 0)
# check if the mask is binary or labelled
- "s_unique = unique(source.s_mask);"
+ s_unique = np.unique(self.source.s_mask)
# create a second indexing variable
- if eng.eval("numel(s_unique) <= 2 && sum(s_unique) == 1"): # noqa: F821
- # set signal index to all elements
- eng.workspace["s_source_sig_index"] = ":" # noqa: F821
+ if np.size(s_unique) <= 2 and np.sum(s_unique) == 1:
+
+ # unlabelled source mask
+
+ # set signal index to all elements, should also be for szz or szz
+ # print("THIS is zero indexed", np.shape(self.source.sxx), np.shape(self.source.syy), np.shape(self.source.szz), np.max(np.shape(self.source.szz)))
+ temp_array = []
+ if self.source.sxx is not None:
+ # temp_array.append(np.max(np.shape(self.source.sxx)))
+ temp_array.append(np.shape(self.source.sxx)[0])
+ if self.source.syy is not None:
+ # temp_array.append(np.max(np.shape(self.source.syy)))
+ temp_array.append(np.shape(self.source.syy)[0])
+ if self.source.szz is not None:
+ # temp_array.append(np.max(np.shape(self.source.szz)))
+ temp_array.append(np.shape(self.source.szz)[0])
+ if self.source.syz is not None:
+ # temp_array.append(np.max(np.shape(self.source.syz)))
+ temp_array.append(np.shape(self.source.sxy)[0])
+ if self.source.sxz is not None:
+ # temp_array.append(np.max(np.shape(self.source.sxz)))
+ temp_array.append(np.shape(self.source.sxz)[0])
+ if self.source.sxy is not None:
+ # temp_array.append(np.max(np.shape(self.source.sxy)))
+ temp_array.append(np.shape(self.source.syz)[0])
+ value: int = np.max(np.asarray(temp_array))
+ print("value:", value)
+ self.s_source_sig_index = np.squeeze(np.arange(0, value) + int(1))
+
+ if self.source_p0_elastic:
+ print("value (source_p0_elastic):", value)
+ self.s_source_sig_index = self.s_source_pos_index
+
+
+ # print("-------->", self.s_source_sig_index)
else:
- # set signal index to the labels (this allows one input signal
- # to be used for each source label)
- s_source_sig_index = source.s_mask(source.s_mask != 0) # noqa
- f"s_source_pos_index = {self.index_data_type}(s_source_pos_index);"
+ # labelled source mask
+
+ # set signal index to the labels (this allows one input signal to be used for each source label)
+ print("THIS is also zero indexed")
+ arr = np.where(self.source.s_mask.flatten(order="F") != 0)[0]
+ self.s_source_sig_index = self.source.s_mask.flatten(order="F")[arr]
+ # self.s_source_sig_index = self.source.s_mask[self.source.s_mask != 0] + int(1) # matlab_find(self.source.s_mask)
+ # self.s_source_sig_index = matlab_find(self.source.s_mask)
+
+ self.s_source_pos_index = np.asarray(self.s_source_pos_index)
+ for i in range(np.shape(self.s_source_pos_index)[0]):
+ self.s_source_pos_index[i] = cast_to_type(self.s_source_pos_index[i], self.index_data_type)
+
if self.source_s_labelled:
- f"s_source_sig_index = {self.index_data_type}(s_source_sig_index);"
+ self.s_source_sig_index = cast_to_type(self.s_source_sig_index, self.index_data_type)
else:
# ----------------------
@@ -996,6 +1257,7 @@ def check_source(self, k_dim, k_Nt) -> None:
# clean up unused variables
del active_elements_mask
+
def check_kgrid_time(self) -> None:
"""
Check time-related kWaveGrid inputs
@@ -1038,6 +1300,7 @@ def check_kgrid_time(self) -> None:
if self.kgrid.dt > dt_stability_limit:
logging.log(logging.WARN, " time step may be too large for a stable simulation.")
+
@staticmethod
def select_precision(opt: SimulationOptions):
"""
@@ -1071,6 +1334,7 @@ def select_precision(opt: SimulationOptions):
raise ValueError("'Unknown ''DataCast'' option'")
return precision
+
def check_input_combinations(self, opt: SimulationOptions, user_medium_density_input: bool, k_dim, pml_size, kgrid_N) -> None:
"""
Check the input combinations for correctness and validity
@@ -1112,7 +1376,7 @@ def check_input_combinations(self, opt: SimulationOptions, user_medium_density_i
if self.record.u_split_field and not self.binary_sensor_mask:
raise ValueError("The option sensor.record = {" "u_split_field" "} is only compatible " "with a binary sensor mask.")
- # check input options for data streaming *****
+ # check input options for data streaming
if opt.stream_to_disk:
if not self.use_sensor or self.time_rev:
raise ValueError(
@@ -1121,9 +1385,9 @@ def check_input_combinations(self, opt: SimulationOptions, user_medium_density_i
" is currently only compatible "
"with forward simulations using a non-zero sensor mask."
)
- elif self.sensor.record is not None and self.sensor.record.ismember(self.record.flags[1:]).any():
+ elif self.sensor.record is not None and np.all([item not in ['p', "p"] for item in self.sensor.record]):
raise ValueError(
- "The optional input " "StreamToDisk" " is currently only compatible " "with sensor.record = {" "p" "} (the default)."
+ "The optional input " "StreamToDisk" " is currently only compatible " "with sensor.record = [" "p" "] (the default)."
)
is_axisymmetric = self.options.simulation_type.is_axisymmetric()
@@ -1149,8 +1413,8 @@ def check_input_combinations(self, opt: SimulationOptions, user_medium_density_i
# check the record start time is within range
record_start_index = self.sensor.record_start_index
- if self.use_sensor and ((record_start_index > self.kgrid.Nt) or (record_start_index < 1)):
- raise ValueError("sensor.record_start_index must be between 1 and the number of time steps.")
+ if self.use_sensor and ((record_start_index > self.kgrid.Nt) or (record_start_index < 0)):
+ raise ValueError("sensor.record_start_index must be between 0 and the number of time steps.")
# ensure 'WSWA' symmetry if using axisymmetric code with 'SaveToDisk'
if is_axisymmetric and self.options.radial_symmetry != "WSWA" and isinstance(self.options.save_to_disk, str):
@@ -1165,7 +1429,10 @@ def check_input_combinations(self, opt: SimulationOptions, user_medium_density_i
# ensure p0 smoothing is switched off if p0 is empty
if not self.source_p0:
+ print("----------------------NO SMOOTHING")
self.options.smooth_p0 = False
+ #else:
+ # print('----------------------(PERHAPS) SMOOTHED!')
# start log if required
if opt.create_log:
@@ -1180,6 +1447,7 @@ def check_input_combinations(self, opt: SimulationOptions, user_medium_density_i
if k.endswith("_DEF"):
delattr(self, k)
+
def smooth_and_enlarge(self, source, k_dim, kgrid_N, opt: SimulationOptions) -> None:
"""
Smooth and enlarge grids
@@ -1194,6 +1462,8 @@ def smooth_and_enlarge(self, source, k_dim, kgrid_N, opt: SimulationOptions) ->
None
"""
+ # print("[SMOOTH] AND ENLARGE")
+
# smooth the initial pressure distribution p0 if required, and then restore
# the maximum magnitude
# NOTE 1: if p0 has any values at the edge of the domain, the smoothing
@@ -1202,7 +1472,7 @@ def smooth_and_enlarge(self, source, k_dim, kgrid_N, opt: SimulationOptions) ->
# exactly zero within the PML
# NOTE 3: for the axisymmetric code, p0 is smoothed assuming WS origin
# symmetry
- if self.source_p0 and self.options.smooth_p0:
+ if self.source_p0:
# update command line status
logging.log(logging.INFO, " smoothing p0 distribution...")
@@ -1228,8 +1498,9 @@ def smooth_and_enlarge(self, source, k_dim, kgrid_N, opt: SimulationOptions) ->
p0_exp[:, 1 : kgrid_N.y] = source.p0
p0_exp[:, kgrid_N.y + 0 : kgrid_N.y * 2 - 2] = np.fliplr(source.p0[:, 1:-1])
- # smooth p0
- p0_exp = smooth(p0_exp, True)
+ # smooth p0 if declared
+ if self.options.smooth_p0:
+ p0_exp = smooth(p0_exp, True)
# trim back to original size
source.p0 = p0_exp[:, 0 : self.kgrid.Ny]
@@ -1238,46 +1509,45 @@ def smooth_and_enlarge(self, source, k_dim, kgrid_N, opt: SimulationOptions) ->
del kgrid_exp
del p0_exp
else:
- source.p0 = smooth(source.p0, True)
+ if (not self.source_p0_elastic) and self.options.smooth_p0:
+ source.p0 = np.squeeze(smooth(source.p0, True))
+ else:
+ print('already smoothed or not declared')
+ pass
# expand the computational grid if the PML is set to be outside the input
# grid defined by the user
if opt.pml_inside is False:
- expand_results = expand_grid_matrices(
- self.kgrid,
- self.medium,
- self.source,
- self.sensor,
- self.options,
- dotdict(
- {
- "p_source_pos_index": self.p_source_pos_index,
- "u_source_pos_index": self.u_source_pos_index,
- "s_source_pos_index": self.s_source_pos_index,
- }
- ),
- dotdict(
- {
- "axisymmetric": self.options.simulation_type.is_axisymmetric(),
- "use_sensor": self.use_sensor,
- "blank_sensor": self.blank_sensor,
- "cuboid_corners": self.cuboid_corners,
- "source_p0": self.source_p0,
- "source_p": self.source_p,
- "source_ux": self.source_ux,
- "source_uy": self.source_uy,
- "source_uz": self.source_uz,
- "transducer_source": self.transducer_source,
- "source_sxx": self.source_sxx,
- "source_syy": self.source_syy,
- "source_szz": self.source_szz,
- "source_sxy": self.source_sxy,
- "source_sxz": self.source_sxz,
- "source_syz": self.source_syz,
- }
- ),
- )
- self.kgrid, self.index_data_type, self.p_source_pos_index, self.u_source_pos_index, self.s_source_pos_index = expand_results
+ values = dotdict({"p_source_pos_index": self.p_source_pos_index,
+ "u_source_pos_index": self.u_source_pos_index,
+ "s_source_pos_index": self.s_source_pos_index,
+ "cuboid_corners_list": self.record.cuboid_corners_list})
+ flags = dotdict({"axisymmetric": self.options.simulation_type.is_axisymmetric(),
+ "use_sensor": self.use_sensor,
+ "blank_sensor": self.blank_sensor,
+ "cuboid_corners": self.cuboid_corners,
+ "source_p0": self.source_p0,
+ "source_p": self.source_p,
+ "source_ux": self.source_ux,
+ "source_uy": self.source_uy,
+ "source_uz": self.source_uz,
+ "transducer_source": self.transducer_source,
+ "source_p0_elastic": self.source_p0_elastic,
+ "source_sxx": self.source_sxx,
+ "source_syy": self.source_syy,
+ "source_szz": self.source_szz,
+ "source_sxy": self.source_sxy,
+ "source_sxz": self.source_sxz,
+ "source_syz": self.source_syz})
+
+ expand_results = expand_grid_matrices(self.kgrid, self.medium, self.source,
+ self.sensor, self.options, values, flags)
+
+ self.kgrid, self.index_data_type, self.p_source_pos_index, self.u_source_pos_index, \
+ self.s_source_pos_index, cuboid_corners_list = expand_results
+
+ self.record.cuboid_corners_list = cuboid_corners_list
+
# get maximum prime factors
if self.options.simulation_type.is_axisymmetric():
@@ -1293,14 +1563,23 @@ def smooth_and_enlarge(self, source, k_dim, kgrid_N, opt: SimulationOptions) ->
del prime_facs
# smooth the sound speed distribution if required
- if opt.smooth_c0 and num_dim2(self.medium.sound_speed) == k_dim and self.medium.sound_speed.size > 1:
- logging.log(logging.INFO, " smoothing sound speed distribution...")
- self.medium.sound_speed = smooth(self.medium.sound_speed)
+ if not self.options.simulation_type.is_elastic_simulation():
+ if opt.smooth_c0 and num_dim2(self.medium.sound_speed) == k_dim and self.medium.sound_speed.size > 1:
+ logging.log(logging.INFO, " smoothing ACOUSTIC sound speed distribution...")
+ self.medium.sound_speed = smooth(self.medium.sound_speed, restore_max=False)
+ else:
+ if opt.smooth_c0 and num_dim2(self.medium.sound_speed_compression) == k_dim and self.medium.sound_speed_compression.size > 1:
+ logging.log(logging.INFO, " smoothing sound speed compression distribution...")
+ self.medium.sound_speed_compression = smooth(self.medium.sound_speed_compression, restore_max=False)
+ if opt.smooth_c0 and num_dim2(self.medium.sound_speed_shear) == k_dim and self.medium.sound_speed_shear.size > 1:
+ logging.log(logging.INFO, " smoothing sound speed shear distribution...")
+ self.medium.sound_speed_shear = smooth(self.medium.sound_speed_shear, restore_max=False)
# smooth the ambient density distribution if required
if opt.smooth_rho0 and num_dim2(self.medium.density) == k_dim and self.medium.density.size > 1:
logging.log(logging.INFO, "smoothing density distribution...")
- self.medium.density = smooth(self.medium.density)
+ self.medium.density = smooth(self.medium.density, restore_max=False)
+
def create_sensor_variables(self) -> None:
"""
@@ -1309,6 +1588,7 @@ def create_sensor_variables(self) -> None:
Returns:
None
"""
+
# define the output variables and mask indices if using the sensor
if self.use_sensor:
if not self.blank_sensor or self.options.save_to_disk:
@@ -1319,36 +1599,46 @@ def create_sensor_variables(self) -> None:
# loop through the list of cuboid corners, and extract the
# sensor mask indices for each cube
for cuboid_index in range(self.record.cuboid_corners_list.shape[1]):
+
# create empty binary mask
temp_mask = np.zeros_like(self.kgrid.k, dtype=bool)
if self.kgrid.dim == 1:
- self.sensor.mask[
+ temp_mask[
self.record.cuboid_corners_list[0, cuboid_index] : self.record.cuboid_corners_list[1, cuboid_index]
- ] = 1
+ ] = True
if self.kgrid.dim == 2:
- self.sensor.mask[
+ temp_mask[
self.record.cuboid_corners_list[0, cuboid_index] : self.record.cuboid_corners_list[2, cuboid_index],
self.record.cuboid_corners_list[1, cuboid_index] : self.record.cuboid_corners_list[3, cuboid_index],
- ] = 1
+ ] = True
if self.kgrid.dim == 3:
- self.sensor.mask[
+ temp_mask[
self.record.cuboid_corners_list[0, cuboid_index] : self.record.cuboid_corners_list[3, cuboid_index],
self.record.cuboid_corners_list[1, cuboid_index] : self.record.cuboid_corners_list[4, cuboid_index],
self.record.cuboid_corners_list[2, cuboid_index] : self.record.cuboid_corners_list[5, cuboid_index],
- ] = 1
+ ] = True
# extract mask indices
- self.sensor_mask_index.append(matlab_find(temp_mask))
- self.sensor_mask_index = np.array(self.sensor_mask_index)
+ temp_mask = np.squeeze(np.where(temp_mask.flatten(order="F"))) + 1 # due to matlab indexing
+
+ self.sensor_mask_index.append(temp_mask)
+
+ self.sensor_mask_index = np.concatenate(self.sensor_mask_index)
+
+ # convert to numpy array
+ self.sensor_mask_index = np.squeeze(np.asarray(self.sensor_mask_index))
# cleanup unused variables
del temp_mask
+ # print("-------------", np.shape(self.sensor_mask_index ) )
+
else:
- # create mask indices (this works for both normal sensor and
- # transducer inputs)
- self.sensor_mask_index = np.where(self.sensor.mask.flatten(order="F") != 0)[0] + 1 # +1 due to matlab indexing
+
+ # print("this is something else - not cuboid corners")
+ # create mask indices (this works for both normal sensor and transducer inputs)
+ self.sensor_mask_index = np.where(self.sensor.mask.flatten(order="F") != 0)[0] + 1 # +1 due to matlab indexing. Use matlab_find?
self.sensor_mask_index = np.expand_dims(self.sensor_mask_index, -1) # compatibility, n => [n, 1]
# convert the data type depending on the number of indices (this saves
@@ -1356,8 +1646,14 @@ def create_sensor_variables(self) -> None:
self.sensor_mask_index = cast_to_type(self.sensor_mask_index, self.index_data_type)
else:
+ print('Use sensor but is not a blank sensor', (not self.blank_sensor))
# set the sensor mask index variable to be empty
- self.sensor_mask_index = []
+ self.sensor_mask_index = None
+ else:
+ print("not using a sensor")
+
+ # print("create_sensor_variables", self.sensor_mask_index)
+
def create_absorption_vars(self) -> None:
"""
@@ -1372,6 +1668,7 @@ def create_absorption_vars(self) -> None:
self.kgrid, self.medium, self.equation_of_state
)
+
def assign_pseudonyms(self, medium: kWaveMedium, kgrid: kWaveGrid) -> None:
"""
Shorten commonly used field names (these act only as pointers provided that the values aren't modified)
@@ -1388,6 +1685,7 @@ def assign_pseudonyms(self, medium: kWaveMedium, kgrid: kWaveGrid) -> None:
self.rho0 = medium.density
self.c0 = medium.sound_speed
+
def scale_source_terms(self, is_scale_source_terms) -> None:
"""
Scale the source terms based on the expanded and smoothed values of the medium parameters
@@ -1445,6 +1743,7 @@ def scale_source_terms(self, is_scale_source_terms) -> None:
),
)
+
def create_pml_indices(self, kgrid_dim, kgrid_N: Vector, pml_size, pml_inside, is_axisymmetric):
"""
Define index variables to remove the PML from the display if the optional
@@ -1483,4 +1782,5 @@ def create_pml_indices(self, kgrid_dim, kgrid_N: Vector, pml_size, pml_inside, i
# the _final and _all output variables if 'PMLInside' is set to false
# if self.record is None:
# self.record = Recorder()
+
self.record.set_index_variables(self.kgrid, pml_size, pml_inside, is_axisymmetric)
diff --git a/kwave/kWaveSimulation_helper/__init__.py b/kwave/kWaveSimulation_helper/__init__.py
index a2eb6e82e..39afec72f 100644
--- a/kwave/kWaveSimulation_helper/__init__.py
+++ b/kwave/kWaveSimulation_helper/__init__.py
@@ -3,5 +3,15 @@
from kwave.kWaveSimulation_helper.expand_grid_matrices import expand_grid_matrices
from kwave.kWaveSimulation_helper.retract_transducer_grid_size import retract_transducer_grid_size
from kwave.kWaveSimulation_helper.save_to_disk_func import save_to_disk_func
+from kwave.kWaveSimulation_helper.save_intensity import save_intensity
from kwave.kWaveSimulation_helper.scale_source_terms_func import scale_source_terms_func
from kwave.kWaveSimulation_helper.set_sound_speed_ref import set_sound_speed_ref
+from kwave.kWaveSimulation_helper.extract_sensor_data import extract_sensor_data
+
+from kwave.kWaveSimulation_helper.reorder_cuboid_corners import reorder_cuboid_corners
+
+from kwave.kWaveSimulation_helper.create_storage_variables import gridDataFast2D, \
+ gridDataFast3D, OutputSensor, create_storage_variables, set_flags, get_num_of_sensor_points, \
+ get_num_recorded_time_points, create_shift_operators, create_normalized_wavenumber_vectors, \
+ create_sensor_variables, create_transducer_buffer, compute_triangulation_points, \
+ calculate_all_vars_size
diff --git a/kwave/kWaveSimulation_helper/create_absorption_variables.py b/kwave/kWaveSimulation_helper/create_absorption_variables.py
index 56ae2d380..545f58727 100644
--- a/kwave/kWaveSimulation_helper/create_absorption_variables.py
+++ b/kwave/kWaveSimulation_helper/create_absorption_variables.py
@@ -10,12 +10,15 @@
def create_absorption_variables(kgrid: kWaveGrid, medium: kWaveMedium, equation_of_state):
# define the lossy derivative operators and proportionality coefficients
+
+ # print("------> equation of state:", equation_of_state)
+
if equation_of_state == "absorbing":
return create_absorbing_medium_variables(kgrid.k, medium)
elif equation_of_state == "stokes":
return create_stokes_medium_variables(medium)
else:
- raise NotImplementedError
+ return None, None, None, None
def create_absorbing_medium_variables(kgrid_k, medium: kWaveMedium):
diff --git a/kwave/kWaveSimulation_helper/create_storage_variables.py b/kwave/kWaveSimulation_helper/create_storage_variables.py
new file mode 100644
index 000000000..d0672697c
--- /dev/null
+++ b/kwave/kWaveSimulation_helper/create_storage_variables.py
@@ -0,0 +1,615 @@
+import numpy as np
+from numpy.fft import ifftshift
+from copy import deepcopy
+from typing import Union, List
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.recorder import Recorder
+from kwave.options.simulation_options import SimulationOptions
+from kwave.utils.dotdictionary import dotdict
+
+
+from scipy.spatial import Delaunay
+
+
+def gridDataFast2D(x, y, xi, yi):
+ """
+ Delauney triangulation in 2D
+ """
+ x = np.ravel(x)
+ y = np.ravel(y)
+ xi = np.ravel(xi)
+ yi = np.ravel(yi)
+
+ points = np.squeeze(np.dstack((x, y)))
+ interpolation_points = np.squeeze(np.dstack((xi, yi)))
+
+ tri = Delaunay(points)
+
+ indices = tri.find_simplex(interpolation_points)
+
+ bc = tri.transform[indices, :2].dot(np.transpose(tri.points[indices, :] - tri.transform[indices, 2]))
+
+ return tri.points[indices, :], bc
+
+
+def gridDataFast3D(x, y, z, xi, yi, zi):
+ """
+ Delauney triangulation in 3D
+ """
+ x = np.ravel(x)
+ y = np.ravel(y)
+ z = np.ravel(z)
+ xi = np.ravel(xi)
+ yi = np.ravel(yi)
+ zi = np.ravel(zi)
+
+ grid_points = np.squeeze(np.dstack((x, y, z)))
+ interpolation_points = np.squeeze(np.dstack((xi, yi, zi)))
+
+ tri = Delaunay(grid_points)
+
+ simplex_indices = tri.find_simplex(interpolation_points)
+
+ print("----------->", tri.simplices[simplex_indices])
+
+ # barycentric coordinates
+ bc = tri.transform[simplex_indices, :2].dot(np.transpose(tri.points[simplex_indices, :] - tri.transform[simplex_indices, 2]))
+
+ print("----------->", bc)
+
+ return tri.points[simplex_indices, :], bc
+
+
+class OutputSensor(object):
+ """
+ Class which holds information about which spatial locations are used to save data
+ """
+ flags = None
+ x_shift_neg = None
+ p = None
+
+
+def create_storage_variables(kgrid: kWaveGrid, sensor, opt: SimulationOptions,
+ values: dotdict, flags: dotdict, record: Recorder):
+ """
+ Creates the storage variable sensor
+ """
+
+ # =========================================================================
+ # PREPARE DATA MASKS AND STORAGE VARIABLES
+ # =========================================================================
+
+ sensor_data = OutputSensor()
+
+ # print("unset flags:")
+ # for k, v in flags.items():
+ # print("\t", k, v)
+ flags = set_flags(flags, values.sensor_x, sensor.mask, opt.cartesian_interp)
+ # print("set flags:")
+ # for k, v in flags.items():
+ # print("\t", k, v)
+
+ # preallocate output variables
+ if flags.time_rev:
+ return flags
+
+ num_sensor_points = get_num_of_sensor_points(flags.blank_sensor,
+ flags.binary_sensor_mask,
+ kgrid.k,
+ values.sensor_mask_index,
+ values.sensor_x)
+
+ num_recorded_time_points, _ = \
+ get_num_recorded_time_points(kgrid.dim, kgrid.Nt, opt.stream_to_disk, sensor.record_start_index)
+
+ record = create_shift_operators(record, values.record, kgrid, opt.use_sg)
+
+ create_normalized_wavenumber_vectors(record, kgrid, flags.record_u_split_field)
+
+ pml_size = [opt.pml_x_size, opt.pml_y_size, opt.pml_z_size]
+ pml_size = Vector(pml_size[:kgrid.dim])
+ all_vars_size = calculate_all_vars_size(kgrid, opt.pml_inside, pml_size)
+
+ sensor_data = create_sensor_variables(values.record, kgrid, num_sensor_points, num_recorded_time_points,
+ all_vars_size, values.sensor_mask_index, flags.use_cuboid_corners)
+
+ create_transducer_buffer(flags.transducer_sensor, values.transducer_receive_elevation_focus, sensor,
+ num_sensor_points, num_recorded_time_points, values.sensor_data_buffer_size,
+ flags, sensor_data)
+
+ # print("pre:", record)
+
+ record = compute_triangulation_points(flags, kgrid, record, sensor.mask)
+
+ # print("post:", record)
+
+ return flags, record, sensor_data, num_recorded_time_points
+
+
+def set_flags(flags: dotdict, sensor_x, sensor_mask, is_cartesian_interp):
+ """
+ check sensor mask based on the Cartesian interpolation setting
+ """
+
+ if not flags.binary_sensor_mask and is_cartesian_interp == 'nearest':
+
+ # extract the data using the binary sensor mask created in
+ # input_checking, but switch on Cartesian reorder flag so that the
+ # final data is returned in the correct order (not in time
+ # reversal mode).
+ flags.binary_sensor_mask = True
+ if not flags.time_rev:
+ flags.reorder_data = True
+
+ # check if any duplicate points have been discarded in the
+ # conversion from a Cartesian to binary mask
+ num_discarded_points = len(sensor_x) - sensor_mask.sum()
+ if num_discarded_points != 0:
+ print(f' WARNING: {num_discarded_points} duplicated sensor points discarded (nearest neighbour interpolation)')
+
+ return flags
+
+
+def get_num_of_sensor_points(is_blank_sensor, is_binary_sensor_mask, kgrid_k, sensor_mask_index, sensor_x):
+ """
+ Returns the number of sensor points for a given set of sensor parameters.
+
+ Args:
+ is_blank_sensor (bool): Whether the sensor is blank or not.
+ is_binary_sensor_mask (bool): Whether the sensor mask is binary or not.
+ kgrid_k (ndarray): An array of k-values for the k-Wave grid.
+ sensor_mask_index (list): List of sensor mask indices.
+ sensor_x (list): List of sensor x-coordinates.
+
+ Returns:
+ int: The number of sensor points.
+ """
+ if is_blank_sensor:
+ # print("0", kgrid_k.shape)
+ num_sensor_points = kgrid_k.size
+ elif is_binary_sensor_mask:
+ # print("1.", len(sensor_mask_index))
+ num_sensor_points = len(sensor_mask_index)
+ else:
+ # print("2.", len(sensor_x), sensor_x)
+ num_sensor_points = len(sensor_x)
+ return num_sensor_points
+
+
+def get_num_recorded_time_points(kgrid_dim, Nt, stream_to_disk, record_start_index):
+ """
+ calculate the number of time points that are stored
+ - if streaming data to disk, reduce to the size of the
+ sensor_data matrix based on the value of self.options.stream_to_disk
+ - if a user input for sensor.record_start_index is given, reduce
+ the size of the sensor_data matrix based on the value given
+ Args:
+ kgrid_dim:
+ Nt:
+ stream_to_disk:
+ record_start_index:
+
+ Returns:
+
+ """
+ if kgrid_dim == 3 and stream_to_disk:
+
+ # set the number of points
+ num_recorded_time_points = stream_to_disk
+
+ # initialise the file index variable
+ stream_data_index = 1
+
+ else:
+ num_recorded_time_points = Nt - record_start_index + 1
+ stream_data_index = None # ???
+
+ return num_recorded_time_points, stream_data_index
+
+
+def create_shift_operators(record: Recorder, record_old: Recorder, kgrid: kWaveGrid, is_use_sg: bool):
+ """
+ create shift operators used for calculating the components of the
+ particle velocity field on the non-staggered grids (these are used
+ for both binary and cartesian sensor masks)
+ """
+
+ if (record_old.u_non_staggered or record_old.u_split_field or record_old.I or record_old.I_avg):
+ if is_use_sg:
+ if kgrid.dim == 1:
+ record.x_shift_neg = ifftshift(np.exp(-1j * kgrid.k_vec.x * kgrid.dx / 2))
+ elif kgrid.dim == 2:
+ record.x_shift_neg = ifftshift(np.exp(-1j * kgrid.k_vec.x * kgrid.dx / 2))
+ record.y_shift_neg = ifftshift(np.exp(-1j * kgrid.k_vec.y * kgrid.dy / 2)).T
+ elif kgrid.dim == 3:
+ record.x_shift_neg = ifftshift(np.exp(-1j * kgrid.k_vec.x * kgrid.dx / 2))
+ record.y_shift_neg = ifftshift(np.exp(-1j * kgrid.k_vec.y * kgrid.dy / 2))
+ record.z_shift_neg = ifftshift(np.exp(-1j * kgrid.k_vec.z * kgrid.dz / 2))
+
+ record.x_shift_neg = np.expand_dims(record.x_shift_neg, axis=-1)
+
+ record.y_shift_neg = np.expand_dims(record.y_shift_neg, axis=0)
+
+ record.z_shift_neg = np.squeeze(record.z_shift_neg)
+ record.z_shift_neg = np.expand_dims(record.z_shift_neg, axis=0)
+ record.z_shift_neg = np.expand_dims(record.z_shift_neg, axis=0)
+
+ else:
+ if kgrid.dim == 1:
+ record.x_shift_neg = 1
+ elif kgrid.dim == 2:
+ record.x_shift_neg = 1
+ record.y_shift_neg = 1
+ elif kgrid.dim == 3:
+ record.x_shift_neg = 1
+ record.y_shift_neg = 1
+ record.z_shift_neg = 1
+ return record
+
+
+def create_normalized_wavenumber_vectors(record: Recorder, kgrid: kWaveGrid, is_record_u_split_field):
+ """
+ create normalised wavenumber vectors for k-space dyadics used to
+ split the particule velocity into compressional and shear components
+ """
+ if not is_record_u_split_field:
+ return record
+
+ # x-dimension
+ record.kx_norm = kgrid.kx / kgrid.k
+ record.kx_norm[kgrid.k == 0] = 0
+ record.kx_norm = ifftshift(record.kx_norm)
+
+ # y-dimension
+ record.ky_norm = kgrid.ky / kgrid.k
+ record.ky_norm[kgrid.k == 0] = 0
+ record.ky_norm = ifftshift(record.ky_norm)
+
+ # z-dimension
+ if kgrid.dim == 3:
+ record.kz_norm = kgrid.kz / kgrid.k
+ record.kz_norm[kgrid.k == 0] = 0
+ record.kz_norm = ifftshift(record.kz_norm)
+
+ return record
+
+
+def create_sensor_variables(record_old: Recorder, kgrid, num_sensor_points, num_recorded_time_points,
+ all_vars_size, sensor_mask_index, use_cuboid_corners) -> Union[dotdict, List[dotdict]]:
+ """
+ create storage and scaling variables - all variables are saved as fields of
+ a container called sensor_data. If cuboid corners are used this is a list, else a dictionary-like container
+ """
+
+ # print("[record_old] (create_sensor_variables)", record_old)
+
+ if use_cuboid_corners:
+
+ # as a list
+ sensor_data = []
+
+ # get number of doctdicts in the list for each set of cuboid corners
+ n_cuboids: int = np.shape(record_old.cuboid_corners_list)[1]
+
+ # for each set of cuboid corners
+ for cuboid_index in np.arange(n_cuboids, dtype=int):
+
+ # add an entry to the list
+ sensor_data.append(dotdict())
+
+ # get size of cuboid for indexing regions of computational grid
+ if kgrid.dim == 1:
+ cuboid_size_x = [record_old.cuboid_corners_list[1, cuboid_index] - record_old.cuboid_corners_list[0, cuboid_index] + 1, 1]
+ elif kgrid.dim == 2:
+ cuboid_size_x = [record_old.cuboid_corners_list[2, cuboid_index] - record_old.cuboid_corners_list[0, cuboid_index] + 1,
+ record_old.cuboid_corners_list[3, cuboid_index] - record_old.cuboid_corners_list[1, cuboid_index] + 1]
+ elif kgrid.dim == 3:
+ cuboid_size_x = [record_old.cuboid_corners_list[3, cuboid_index] - record_old.cuboid_corners_list[0, cuboid_index] + 1,
+ record_old.cuboid_corners_list[4, cuboid_index] - record_old.cuboid_corners_list[1, cuboid_index] + 1,
+ record_old.cuboid_corners_list[5, cuboid_index] - record_old.cuboid_corners_list[2, cuboid_index] + 1]
+
+ cuboid_size_xt = deepcopy(cuboid_size_x)
+ cuboid_size_xt.append(num_recorded_time_points)
+
+ # time history of the acoustic pressure
+ if record_old.p or record_old.I or record_old.I_avg:
+ sensor_data[cuboid_index].p = np.zeros(cuboid_size_xt)
+
+ # maximum pressure
+ if record_old.p_max:
+ sensor_data[cuboid_index].p_max = np.zeros(cuboid_size_x)
+
+ # minimum pressure
+ if record_old.p_min:
+ sensor_data[cuboid_index].p_min = np.zeros(cuboid_size_x)
+
+ # rms pressure
+ if record_old.p_rms:
+ sensor_data[cuboid_index].p_rms = np.zeros(cuboid_size_x)
+
+ # time history of the acoustic particle velocity
+ if record_old.u:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 1:
+ sensor_data[cuboid_index].ux = np.zeros(cuboid_size_xt)
+ elif kgrid.dim == 2:
+ sensor_data[cuboid_index].ux = np.zeros(cuboid_size_xt)
+ sensor_data[cuboid_index].uy = np.zeros(cuboid_size_xt)
+ elif kgrid.dim == 3:
+ sensor_data[cuboid_index].ux = np.zeros(cuboid_size_xt)
+ sensor_data[cuboid_index].uy = np.zeros(cuboid_size_xt)
+ sensor_data[cuboid_index].uz = np.zeros(cuboid_size_xt)
+
+ # store the time history of the particle velocity on staggered grid
+ if record_old.u_non_staggered or record_old.I or record_old.I_avg:
+ # print("record_old is correct")
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 1:
+ sensor_data[cuboid_index].ux_non_staggered = np.zeros(cuboid_size_xt)
+ elif kgrid.dim == 2:
+ sensor_data[cuboid_index].ux_non_staggered = np.zeros(cuboid_size_xt)
+ sensor_data[cuboid_index].uy_non_staggered = np.zeros(cuboid_size_xt)
+ elif kgrid.dim == 3:
+ # print("THIS MUST BE SET")
+ sensor_data[cuboid_index].ux_non_staggered = np.zeros(cuboid_size_xt)
+ sensor_data[cuboid_index].uy_non_staggered = np.zeros(cuboid_size_xt)
+ sensor_data[cuboid_index].uz_non_staggered = np.zeros(cuboid_size_xt)
+
+ # time history of the acoustic particle velocity split into compressional and shear components
+ if record_old.u_split_field:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 2:
+ sensor_data[cuboid_index].ux_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data[cuboid_index].ux_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data[cuboid_index].uy_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data[cuboid_index].uy_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+ if kgrid.dim == 3:
+ sensor_data[cuboid_index].ux_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data[cuboid_index].ux_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data[cuboid_index].uy_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data[cuboid_index].uy_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data[cuboid_index].uz_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data[cuboid_index].uz_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+
+ else:
+
+ # allocate empty sensor
+ sensor_data = dotdict()
+
+ # if only p is being stored (i.e., if no user input is given for
+ # sensor.record), then sensor_data.p is copied to sensor_data at the
+ # end of the simulation
+
+ # time history of the acoustic pressure
+ if record_old.p or record_old.I or record_old.I_avg:
+ # print("create storage:", num_sensor_points, num_recorded_time_points, np.shape(sensor_data.p) )
+ # print("should be here", num_sensor_points, num_recorded_time_points)
+ sensor_data.p = np.zeros([num_sensor_points, num_recorded_time_points])
+
+ # maximum pressure
+ if record_old.p_max:
+ sensor_data.p_max = np.zeros([num_sensor_points,])
+
+ # minimum pressure
+ if record_old.p_min:
+ sensor_data.p_min = np.zeros([num_sensor_points,])
+
+ # rms pressure
+ if record_old.p_rms:
+ sensor_data.p_rms = np.zeros([num_sensor_points,])
+
+ # maximum pressure over all grid points
+ if record_old.p_max_all:
+ sensor_data.p_max_all = np.zeros(all_vars_size)
+
+ # minimum pressure over all grid points
+ if record_old.p_min_all:
+ sensor_data.p_min_all = np.zeros(all_vars_size)
+
+ # time history of the acoustic particle velocity
+ if record_old.u:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 1:
+ sensor_data.ux = np.zeros([num_sensor_points, num_recorded_time_points])
+ elif kgrid.dim == 2:
+ sensor_data.ux = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uy = np.zeros([num_sensor_points, num_recorded_time_points])
+ elif kgrid.dim == 3:
+ sensor_data.ux = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uy = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uz = np.zeros([num_sensor_points, num_recorded_time_points])
+
+ # maximum particle velocity
+ if record_old.u_max:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 1:
+ sensor_data.ux_max = np.zeros([num_sensor_points,])
+ if kgrid.dim == 2:
+ sensor_data.ux_max = np.zeros([num_sensor_points,])
+ sensor_data.uy_max = np.zeros([num_sensor_points,])
+ if kgrid.dim == 3:
+ sensor_data.ux_max = np.zeros([num_sensor_points,])
+ sensor_data.uy_max = np.zeros([num_sensor_points,])
+ sensor_data.uz_max = np.zeros([num_sensor_points,])
+
+ # minimum particle velocity
+ if record_old.u_min:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 1:
+ sensor_data.ux_min = np.zeros([num_sensor_points,])
+ if kgrid.dim == 2:
+ sensor_data.ux_min = np.zeros([num_sensor_points,])
+ sensor_data.uy_min = np.zeros([num_sensor_points,])
+ if kgrid.dim == 3:
+ sensor_data.ux_min = np.zeros([num_sensor_points,])
+ sensor_data.uy_min = np.zeros([num_sensor_points,])
+ sensor_data.uz_min = np.zeros([num_sensor_points,])
+
+ # rms particle velocity
+ if record_old.u_rms:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 1:
+ sensor_data.ux_rms = np.zeros([num_sensor_points,])
+ if kgrid.dim == 2:
+ sensor_data.ux_rms = np.zeros([num_sensor_points,])
+ sensor_data.uy_rms = np.zeros([num_sensor_points,])
+ if kgrid.dim == 3:
+ sensor_data.ux_rms = np.zeros([num_sensor_points,])
+ sensor_data.uy_rms = np.zeros([num_sensor_points,])
+ sensor_data.uz_rms = np.zeros([num_sensor_points,])
+
+ # maximum particle velocity over all grid points
+ if record_old.u_max_all:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 1:
+ sensor_data.ux_max_all = np.zeros(all_vars_size)
+ if kgrid.dim == 2:
+ sensor_data.ux_max_all = np.zeros(all_vars_size)
+ sensor_data.uy_max_all = np.zeros(all_vars_size)
+ if kgrid.dim == 3:
+ sensor_data.ux_max_all = np.zeros(all_vars_size)
+ sensor_data.uy_max_all = np.zeros(all_vars_size)
+ sensor_data.uz_max_all = np.zeros(all_vars_size)
+
+ # minimum particle velocity over all grid points
+ if record_old.u_min_all:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 1:
+ sensor_data.ux_min_all = np.zeros(all_vars_size)
+ if kgrid.dim == 2:
+ sensor_data.ux_min_all = np.zeros(all_vars_size)
+ sensor_data.uy_min_all = np.zeros(all_vars_size)
+ if kgrid.dim == 3:
+ sensor_data.ux_min_all = np.zeros(all_vars_size)
+ sensor_data.uy_min_all = np.zeros(all_vars_size)
+ sensor_data.uz_min_all = np.zeros(all_vars_size)
+
+ # time history of the acoustic particle velocity on the non-staggered grid points
+ if record_old.u_non_staggered or record_old.I or record_old.I_avg:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 1:
+ sensor_data.ux_non_staggered = np.zeros([num_sensor_points, num_recorded_time_points])
+ if kgrid.dim == 2:
+ sensor_data.ux_non_staggered = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uy_non_staggered = np.zeros([num_sensor_points, num_recorded_time_points])
+ if kgrid.dim == 3:
+ sensor_data.ux_non_staggered = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uy_non_staggered = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uz_non_staggered = np.zeros([num_sensor_points, num_recorded_time_points])
+
+ # time history of the acoustic particle velocity split into compressional and shear components
+ if record_old.u_split_field:
+ # pre-allocate the velocity fields based on the number of dimensions in the simulation
+ if kgrid.dim == 2:
+ sensor_data.ux_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.ux_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uy_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uy_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+ if kgrid.dim == 3:
+ sensor_data.ux_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.ux_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uy_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uy_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uz_split_p = np.zeros([num_sensor_points, num_recorded_time_points])
+ sensor_data.uz_split_s = np.zeros([num_sensor_points, num_recorded_time_points])
+
+ # if use_cuboid_corners:
+ # info = "using cuboid_corners (create storage variables)," + str(len(sensor_data)) + ", " + str(np.shape(sensor_data[0].p))
+ # else:
+ # info = "binary_mask (create storage variables), ", np.shape(sensor_data.p)
+ # print("end here (create storage variables)", info)
+
+ return sensor_data
+
+
+def create_transducer_buffer(is_transducer_sensor, is_transducer_receive_elevation_focus, sensor,
+ num_sensor_points, num_recorded_time_points, sensor_data_buffer_size,
+ flags, sensor_data):
+ # object of the kWaveTransducer class is being used as a sensor
+
+ if is_transducer_sensor:
+ if is_transducer_receive_elevation_focus:
+
+ # if there is elevation focusing, a buffer is
+ # needed to store a short time history at each
+ # sensor point before averaging
+ # ???
+ sensor_data_buffer_size = sensor.elevation_beamforming_delays.max() + 1
+ if sensor_data_buffer_size > 1:
+ sensor_data_buffer = np.zeros([num_sensor_points, sensor_data_buffer_size]) # noqa: F841
+ else:
+ del sensor_data_buffer_size
+ flags.transducer_receive_elevation_focus = False
+
+ # the grid points can be summed on the fly and so the
+ # sensor is the size of the number of active elements
+ sensor_data.transducer = np.zeros([int(sensor.number_active_elements), num_recorded_time_points])
+ else:
+ pass
+
+
+def compute_triangulation_points(flags, kgrid, record, mask):
+ """
+ precomputate the triangulation points if a Cartesian sensor mask is used
+ with linear interpolation (tri and bc are the Delaunay TRIangulation and
+ Barycentric Coordinates)
+ """
+
+ if not flags.binary_sensor_mask:
+
+ if kgrid.dim == 1:
+
+ # assign pseudonym for Cartesain grid points in 1D (this is later used for data casting)
+ record.grid_x = kgrid.x_vec
+
+ else:
+
+ if kgrid.dim == 1:
+ # align sensor data as a column vector to be the same as kgrid.x_vec
+ # so that calls to interp return data in the correct dimension
+ sensor_x = np.reshape((mask, (-1, 1)))
+ elif kgrid.dim == 2:
+ sensor_x = mask[0, :]
+ sensor_y = mask[1, :]
+ elif kgrid.dim == 3:
+ sensor_x = mask[0, :]
+ sensor_y = mask[1, :]
+ sensor_z = mask[2, :]
+
+ # update command line status
+ print(' calculating Delaunay triangulation...')
+
+ # compute triangulation
+ if kgrid.dim == 2:
+ if flags.axisymmetric:
+ record.tri, record.bc = gridDataFast2D(kgrid.x, kgrid.y - kgrid.y_vec.min(), sensor_x, sensor_y)
+ else:
+ record.tri, record.bc = gridDataFast2D(kgrid.x, kgrid.y, sensor_x, sensor_y)
+ elif kgrid.dim == 3:
+ record.tri, record.bc = gridDataFast3D(kgrid.x, kgrid.y, kgrid.z, sensor_x, sensor_y, sensor_z)
+
+ print("done")
+
+ return record
+
+
+def calculate_all_vars_size(kgrid, is_pml_inside, pml_size):
+ """
+ calculate the size of the _all and _final output variables - if the
+ PML is set to be outside the grid, these will be the same size as the
+ user input, rather than the expanded grid
+ """
+ if is_pml_inside:
+ all_vars_size = kgrid.k.shape
+ else:
+ if kgrid.dim == 1:
+ all_vars_size = [kgrid.Nx - 2 * pml_size.x, 1]
+ elif kgrid.dim == 2:
+ all_vars_size = [kgrid.Nx - 2 * pml_size.x, kgrid.Ny - 2 * pml_size.y]
+ elif kgrid.dim == 3:
+ all_vars_size = [kgrid.Nx - 2 * pml_size.x, kgrid.Ny - 2 * pml_size.y, kgrid.Nz - 2 * pml_size.z]
+ else:
+ raise NotImplementedError
+ return all_vars_size
diff --git a/kwave/kWaveSimulation_helper/display_simulation_params.py b/kwave/kWaveSimulation_helper/display_simulation_params.py
index 4fa5601d3..8395ec082 100644
--- a/kwave/kWaveSimulation_helper/display_simulation_params.py
+++ b/kwave/kWaveSimulation_helper/display_simulation_params.py
@@ -21,7 +21,7 @@ def display_simulation_params(kgrid: kWaveGrid, medium: kWaveMedium, elastic_cod
_, scale, _, _ = scale_SI(np.min(k_size[k_size != 0]))
print_grid_size(kgrid, scale)
- print_max_supported_freq(kgrid, c_min)
+ print_max_supported_freq(kgrid, c_min, c_min_comp, c_min_shear)
def get_min_sound_speed(medium, is_elastic_code):
@@ -30,10 +30,24 @@ def get_min_sound_speed(medium, is_elastic_code):
if not is_elastic_code:
c_min = np.min(medium.sound_speed)
return c_min, None, None
- else: # pragma: no cover
+ else:
+ c_min = np.min(medium.sound_speed)
c_min_comp = np.min(medium.sound_speed_compression)
- c_min_shear = np.min(medium.sound_speed_shear[medium.sound_speed_shear != 0])
- return None, c_min_comp, c_min_shear
+ # if a array
+ if not np.isscalar(medium.sound_speed_shear):
+ temp = medium.sound_speed_shear[np.where(np.abs(medium.sound_speed_shear) > 1e-6)]
+ if len(temp) > 0:
+ c_min_shear = np.min(temp)
+ else:
+ c_min_shear = None
+ #raise RuntimeWarning("c_min_shear is zero")
+ else:
+ c_min_shear = np.min(medium.sound_speed_shear)
+ if np.isclose(c_min_shear, 0.0):
+ c_min_shear = None
+ #raise RuntimeWarning("c_min_shear is zero")
+
+ return c_min, c_min_comp, c_min_shear
def print_grid_size(kgrid, scale):
@@ -61,7 +75,7 @@ def print_grid_size(kgrid, scale):
logging.log(logging.INFO, f" input grid size: {grid_size_str} grid points ({grid_scale_str}m)")
-def print_max_supported_freq(kgrid, c_min):
+def print_max_supported_freq(kgrid, c_min, c_min_comp=None, c_min_shear=None):
# display the grid size and maximum supported frequency
k_max, k_max_all = kgrid.k_max, kgrid.k_max_all
@@ -89,3 +103,35 @@ def max_freq_str(kfreq):
logging.INFO,
f" maximum supported frequency: {max_freq_str(k_max.x)}Hz by {max_freq_str(k_max.y)}Hz by {max_freq_str(k_max.z)}Hz",
)
+
+ if (c_min_comp is not None and c_min_shear is not None):
+ if kgrid.dim == 1:
+ # display maximum supported frequency
+ logging.log(logging.INFO, " maximum supported shear frequency: ", scale_SI(k_max_all * c_min_shear / (2 * np.pi))[0], "Hz")
+
+ elif kgrid.dim == 2:
+ # display maximum supported frequency
+ if k_max.x == k_max.y:
+ logging.log(logging.INFO, " maximum supported shear frequency: ", scale_SI(k_max_all * c_min_shear / (2 * np.pi))[0], "Hz")
+ else:
+ logging.log(
+ logging.INFO,
+ " maximum supported shear frequency: ",
+ scale_SI(k_max.x * c_min_shear / (2 * np.pi))[0],
+ "Hz by ",
+ scale_SI(k_max.y * c_min_shear / (2 * np.pi))[0],
+ "Hz",
+ )
+
+ elif kgrid.dim == 3:
+ # display maximum supported frequency
+ if k_max.x == k_max.z and k_max.x == k_max.y:
+ logging.log(logging.INFO, " maximum supported shear frequency: ", f"{scale_SI(k_max_all * c_min / (2*np.pi))[0]}Hz")
+ else:
+ logging.log(
+ logging.INFO,
+ " maximum supported shear frequency: ",
+ f"{scale_SI(k_max.x * c_min_shear / (2*np.pi))[0]}Hz by "
+ f"{scale_SI(k_max.y * c_min_shear / (2*np.pi))[0]}Hz by "
+ f"{scale_SI(k_max.z * c_min_shear / (2*np.pi))[0]}Hz",
+ )
diff --git a/kwave/kWaveSimulation_helper/expand_grid_matrices.py b/kwave/kWaveSimulation_helper/expand_grid_matrices.py
index bd9d033a0..9c7aab4bb 100644
--- a/kwave/kWaveSimulation_helper/expand_grid_matrices.py
+++ b/kwave/kWaveSimulation_helper/expand_grid_matrices.py
@@ -12,7 +12,8 @@
from kwave.utils.matrix import expand_matrix
-def expand_grid_matrices(kgrid: kWaveGrid, medium: kWaveMedium, source, sensor, opt: SimulationOptions, values: dotdict, flags: dotdict):
+def expand_grid_matrices(kgrid: kWaveGrid, medium: kWaveMedium, source, sensor,
+ opt: SimulationOptions, values: dotdict, flags: dotdict):
# update command line status
logging.log(logging.INFO, " expanding computational grid...")
@@ -37,6 +38,7 @@ def expand_grid_matrices(kgrid: kWaveGrid, medium: kWaveMedium, source, sensor,
# expand the computational grid, replacing the original grid
kgrid = expand_kgrid(kgrid, flags.axisymmetric, pml_size)
+ # calculate the size of the expanded kgrid to pass
expand_size = calculate_expand_size(kgrid, flags.axisymmetric, pml_size)
# update the data type in case adding the PML requires additional index precision
@@ -46,7 +48,7 @@ def expand_grid_matrices(kgrid: kWaveGrid, medium: kWaveMedium, source, sensor,
expand_sensor(sensor, expand_size, flags.use_sensor, flags.blank_sensor)
# TODO why it is not self.record ? "self"
- record = expand_cuboid_corner_list(flags.cuboid_corners, kgrid, pml_size) # noqa: F841
+ cuboid_corners = expand_cuboid_corner_list(flags.cuboid_corners, values.cuboid_corners_list, kgrid, pml_size)
expand_medium(medium, expand_size)
@@ -58,7 +60,7 @@ def expand_grid_matrices(kgrid: kWaveGrid, medium: kWaveMedium, source, sensor,
print_grid_size(kgrid)
- return kgrid, index_data_type, p_source_pos_index, u_source_pos_index, s_source_pos_index
+ return kgrid, index_data_type, p_source_pos_index, u_source_pos_index, s_source_pos_index, cuboid_corners
def expand_kgrid(kgrid, is_axisymmetric, pml_size):
@@ -86,7 +88,7 @@ def expand_kgrid(kgrid, is_axisymmetric, pml_size):
def calculate_expand_size(kgrid, is_axisymmetric, pml_size):
- # set the PML size for use with expandMatrix, don't expand the inner radial
+ # set the PML size for use with expand_matrix, don't expand the inner radial
# dimension if using the axisymmetric code
if kgrid.dim == 1:
expand_size = pml_size[0]
@@ -103,12 +105,12 @@ def calculate_expand_size(kgrid, is_axisymmetric, pml_size):
def expand_medium(medium: kWaveMedium, expand_size):
- # enlarge the sound speed grids by exting the edge values into the expanded grid
+ # enlarge the sound speed grids by extending the edge values into the expanded grid
medium.sound_speed = np.atleast_1d(medium.sound_speed)
if medium.sound_speed.size > 1:
medium.sound_speed = expand_matrix(medium.sound_speed, expand_size)
- # enlarge the grid of density by exting the edge values into the expanded grid
+ # enlarge the grid of density by extending the edge values into the expanded grid
medium.density = np.atleast_1d(medium.density)
if medium.density.size > 1:
medium.density = expand_matrix(medium.density, expand_size)
@@ -121,6 +123,20 @@ def expand_medium(medium: kWaveMedium, expand_size):
attr = expand_matrix(np.atleast_1d(attr), expand_size)
setattr(medium, key, attr)
+ for key in ["sound_speed_shear", "sound_speed_compression"]:
+ # enlarge the grid of medium[key] if given
+ attr = getattr(medium, key)
+ if attr is not None and np.atleast_1d(attr).size > 1:
+ attr = expand_matrix(np.atleast_1d(attr), expand_size)
+ setattr(medium, key, attr)
+
+ for key in ["alpha_coeff_shear", "alpha_coeff_compression"]:
+ # enlarge the grid of medium[key] if given
+ attr = getattr(medium, key)
+ if attr is not None and np.atleast_1d(attr).size > 1:
+ attr = expand_matrix(np.atleast_1d(attr), expand_size)
+ setattr(medium, key, attr)
+
# enlarge the absorption filter mask if given
if medium.alpha_filter is not None:
medium.alpha_filter = expand_matrix(medium.alpha_filter, expand_size, 0)
@@ -192,20 +208,41 @@ def expand_velocity_sources(
"""
if is_source_ux or is_source_uy or is_source_uz or is_transducer_source:
+
# update the source indexing variable
if isinstance(source, NotATransducer):
# check if the sensor is also the same transducer, if so, don't expand the grid again
if not is_source_sensor_same:
# expand the transducer mask
source.expand_grid(expand_size)
-
# get the new active elements mask
active_elements_mask = source.active_elements_mask
-
# update the indexing variable corresponding to the active elements
u_source_pos_index = matlab_find(active_elements_mask)
+
else:
+
+ # if source.u_mask.ndim == 1:
+ # exp_size = np.asarray( ((expand_size[0]//2, expand_size[0]//2),) )
+ # elif source.u_mask.ndim == 2:
+ # exp_size = np.asarray( ((expand_size[0]//2, expand_size[0]//2),
+ # (expand_size[1]//2, expand_size[1]//2)) )
+ # elif source.u_mask.ndim == 3:
+ # exp_size = np.asarray( ((expand_size[0]//2, expand_size[0]//2),
+ # (expand_size[1]//2, expand_size[1]//2),
+ # (expand_size[2]//2, expand_size[2]//2), ) )
+
+ # if np.max(matlab_find(source.u_mask)) == np.size(source.ux):
+ # print("CHANGING ux")
+ # source.ux = np.pad(source.ux, pad_width=exp_size)
+
+ # if np.max(matlab_find(source.u_mask)) == np.size(source.uy):
+ # source.uy = np.pad(source.uy, pad_width=exp_size)
+ # else:
+ # print("NOT CHANGING")
+
# enlarge the velocity source mask
+ # source.u_mask = np.pad(source.u_mask, pad_width=exp_size)
source.u_mask = expand_matrix(source.u_mask, expand_size, 0)
# create an indexing variable corresponding to the source elements
@@ -217,13 +254,19 @@ def expand_velocity_sources(
def expand_stress_sources(source, expand_size, flags, index_data_type, s_source_pos_index):
+
+ if flags.source_p0_elastic:
+ source.sxx = expand_matrix(source.sxx, expand_size, 0)
+ source.syy = expand_matrix(source.syy, expand_size, 0)
+ return None
+
# enlarge the stress source mask if given
if flags.source_sxx or flags.source_syy or flags.source_szz or flags.source_sxy or flags.source_sxz or flags.source_syz:
- # enlarge the velocity source mask
+ # enlarge the stress source mask
source.s_mask = expand_matrix(source.s_mask, expand_size, 0)
# create an indexing variable corresponding to the source elements
- s_source_pos_index = matlab_find(source.s_mask != 0)
+ s_source_pos_index = matlab_find(source.s_mask)
# convert the data type deping on the number of indices
s_source_pos_index = s_source_pos_index.astype(index_data_type)
@@ -268,30 +311,35 @@ def print_grid_size(kgrid):
logging.log(logging.INFO, " computational grid size:", int(k_Nx), "by", int(k_Ny), "by", int(k_Nz), "grid points")
-def expand_cuboid_corner_list(is_cuboid_list, kgrid, pml_size: Vector):
+def expand_cuboid_corner_list(is_cuboid_corners, cuboid_corners_list, kgrid, pml_size: Vector):
"""
add the PML size to cuboid corner indices if using a cuboid sensor mask
Args:
- is_cuboid_list:
- kgrid:
+ is_cuboid_list: boolean which says whether expanded
+ cuboid_corners_list: the cuboid corners
+ kgrid: the grid, which contains the dimension
+ pml_size: the size of the pml
Returns:
"""
- if not is_cuboid_list:
+ if not is_cuboid_corners or cuboid_corners_list is None:
return
+ cuboid_corners_list = np.asarray(cuboid_corners_list)
+
record = dotdict()
+ record.cuboid_corners_list = cuboid_corners_list
if kgrid.dim == 1:
- record.cuboid_corners_list = record.cuboid_corners_list + pml_size.x
+ record.cuboid_corners_list = cuboid_corners_list + pml_size.x
elif kgrid.dim == 2:
- record.cuboid_corners_list[[0, 2], :] = record.cuboid_corners_list[[0, 2], :] + pml_size.x
- record.cuboid_corners_list[[1, 3], :] = record.cuboid_corners_list[[1, 3], :] + pml_size.y
+ record.cuboid_corners_list[[0, 2], :] = cuboid_corners_list[[0, 2], :] + pml_size.x
+ record.cuboid_corners_list[[1, 3], :] = cuboid_corners_list[[1, 3], :] + pml_size.y
elif kgrid.dim == 3:
- record.cuboid_corners_list[[0, 3], :] = record.cuboid_corners_list[[0, 3], :] + pml_size.x
- record.cuboid_corners_list[[1, 4], :] = record.cuboid_corners_list[[1, 4], :] + pml_size.y
- record.cuboid_corners_list[[2, 5], :] = record.cuboid_corners_list[[2, 5], :] + pml_size.z
- return record
+ record.cuboid_corners_list[[0, 3], :] = cuboid_corners_list[[0, 3], :] + pml_size.x
+ record.cuboid_corners_list[[1, 4], :] = cuboid_corners_list[[1, 4], :] + pml_size.y
+ record.cuboid_corners_list[[2, 5], :] = cuboid_corners_list[[2, 5], :] + pml_size.z
+ return record.cuboid_corners_list
def expand_sensor(sensor, expand_size, is_use_sensor, is_blank_sensor):
diff --git a/kwave/kWaveSimulation_helper/extract_sensor_data.py b/kwave/kWaveSimulation_helper/extract_sensor_data.py
new file mode 100644
index 000000000..820afa80f
--- /dev/null
+++ b/kwave/kWaveSimulation_helper/extract_sensor_data.py
@@ -0,0 +1,746 @@
+import numpy as np
+
+def extract_sensor_data(dim: int, sensor_data, file_index, sensor_mask_index,
+ flags, record, p, ux_sgx, uy_sgy=None, uz_sgz=None):
+ """
+ extract_sensor_data Sample field variables at the sensor locations.
+
+ DESCRIPTION:
+ extract_sensor_data extracts the required sensor data from the acoustic
+ and elastic field variables at each time step. This is defined as a
+ function rather than a script to avoid the computational overhead of
+ using scripts to access variables local to another function. For
+ k-Wave < V1.1, this code was included directly in the simulation
+ functions.
+
+ ABOUT:
+ author - Bradley Treeby
+ date - 9th July 2013
+ last update - 8th November 2018
+
+ This function is part of the k-Wave Toolbox (http://www.k-wave.org)
+ Copyright (C) 2013-2018 Bradley Treeby
+
+ This file is part of k-Wave. k-Wave is free software: you can
+ redistribute it and/or modify it under the terms of the GNU Lesser
+ General Public License as published by the Free Software Foundation,
+ either version 3 of the License, or (at your option) any later version.
+
+ k-Wave is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with k-Wave. If not, see .
+ """
+
+ # =========================================================================
+ # GRID STAGGERING
+ # =========================================================================
+
+ # print("Recorder:", record, dir(record))
+
+ # shift the components of the velocity field onto the non-staggered
+ # grid if required for output
+ if (flags.record_u_non_staggered or flags.record_I or flags.record_I_avg):
+ if (dim == 1):
+ ux_shifted = np.real(np.fft.ifft(record.x_shift_neg * np.fft.fft(ux_sgx, axis=0), axis=0))
+ elif (dim == 2):
+ ux_shifted = np.real(np.fft.ifft(record.x_shift_neg * np.fft.fft(ux_sgx, axis=0), axis=0))
+ uy_shifted = np.real(np.fft.ifft(record.y_shift_neg * np.fft.fft(uy_sgy, axis=1), axis=1))
+ elif (dim == 3):
+ ux_shifted = np.real(np.fft.ifft(record.x_shift_neg * np.fft.fft(ux_sgx, axis=0), axis=0))
+ uy_shifted = np.real(np.fft.ifft(record.y_shift_neg * np.fft.fft(uy_sgy, axis=1), axis=1))
+ uz_shifted = np.real(np.fft.ifft(record.z_shift_neg * np.fft.fft(uz_sgz, axis=2), axis=2))
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # =========================================================================
+ # BINARY SENSOR MASK
+ # =========================================================================
+
+ if flags.binary_sensor_mask and not flags.use_cuboid_corners:
+
+ # store the time history of the acoustic pressure
+ if (flags.record_p or flags.record_I or flags.record_I_avg):
+ if not flags.compute_directivity:
+ sensor_data.p[:, file_index] = np.squeeze(p[np.unravel_index(sensor_mask_index, np.shape(p), order='F')])
+ # print("Should not be doing this!")
+ else:
+ raise NotImplementedError('directivity not used at the moment')
+
+ # store the maximum acoustic pressure
+ if flags.record_p_max:
+ if file_index == 0:
+ sensor_data.p_max = p[np.unravel_index(np.squeeze(sensor_mask_index), np.shape(p), order='F')]
+ else:
+ sensor_data.p_max = np.maximum(sensor_data.p_max,
+ p[np.unravel_index(np.squeeze(sensor_mask_index), np.shape(p), order='F')])
+
+ # store the minimum acoustic pressure
+ if flags.record_p_min:
+ if file_index == 0:
+ sensor_data.p_min = p[np.unravel_index(np.squeeze(sensor_mask_index), np.shape(p), order='F')]
+ else:
+ sensor_data.p_min = np.minimum(sensor_data.p_min,
+ p[np.unravel_index(np.squeeze(sensor_mask_index), np.shape(p), order='F')])
+
+ # store the rms acoustic pressure
+ if flags.record_p_rms:
+ if file_index == 0:
+ sensor_data.p_rms = p[np.unravel_index(np.squeeze(sensor_mask_index), np.shape(p), order='F')]**2
+ else:
+ sensor_data.p_rms = np.sqrt((sensor_data.p_rms**2 * file_index +
+ p[np.unravel_index(np.squeeze(sensor_mask_index), np.shape(p), order='F')]**2) / (file_index + 1) )
+
+ # store the time history of the particle velocity on the staggered grid
+ if flags.record_u:
+ if (dim == 1):
+ sensor_data.ux[:, file_index] = ux_sgx[sensor_mask_index]
+ elif (dim == 2):
+ sensor_data.ux[:, file_index] = ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')]
+ sensor_data.uy[:, file_index] = uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')]
+ elif (dim == 3):
+ sensor_data.ux[:, file_index] = ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), np.shape(ux_sgx), order='F')]
+ sensor_data.uy[:, file_index] = uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), np.shape(uy_sgy), order='F')]
+ sensor_data.uz[:, file_index] = uz_sgz[np.unravel_index(np.squeeze(sensor_mask_index), np.shape(uz_sgz), order='F')]
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the time history of the particle velocity
+ if flags.record_u_non_staggered or flags.record_I or flags.record_I_avg:
+ if (dim == 1):
+ sensor_data.ux_non_staggered[:, file_index] = ux_shifted[sensor_mask_index]
+ elif (dim == 2):
+ sensor_data.ux_non_staggered[:, file_index] = ux_shifted[np.unravel_index(np.squeeze(sensor_mask_index), ux_shifted.shape, order='F')]
+ sensor_data.uy_non_staggered[:, file_index] = uy_shifted[np.unravel_index(np.squeeze(sensor_mask_index), uy_shifted.shape, order='F')]
+ elif (dim == 3):
+ sensor_data.ux_non_staggered[:, file_index] = ux_shifted[np.unravel_index(np.squeeze(sensor_mask_index), ux_shifted.shape, order='F')]
+ sensor_data.uy_non_staggered[:, file_index] = uy_shifted[np.unravel_index(np.squeeze(sensor_mask_index), uy_shifted.shape, order='F')]
+ sensor_data.uz_non_staggered[:, file_index] = uz_shifted[np.unravel_index(np.squeeze(sensor_mask_index), uz_shifted.shape, order='F')]
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the split components of the particle velocity
+ if flags.record_u_split_field:
+ if (dim == 2):
+
+ # compute forward FFTs
+ ux_k = record.x_shift_neg * np.fft.fftn(ux_sgx)
+ uy_k = record.y_shift_neg * np.fft.fftn(uy_sgy)
+
+ # ux compressional
+ split_field = np.real(np.fft.ifftn(record.kx_norm**2 * ux_k + record.kx_norm * record.ky_norm * uy_k))
+ sensor_data.ux_split_p[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # ux shear
+ split_field = np.real(np.fft.ifftn((1.0 - record.kx_norm**2) * ux_k - record.kx_norm * record.ky_norm * uy_k))
+ sensor_data.ux_split_s[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # uy compressional
+ split_field = np.real(np.fft.ifftn(record.ky_norm * record.kx_norm * ux_k + record.ky_norm **2 * uy_k))
+ sensor_data.uy_split_p[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # uy shear
+ split_field = np.real(np.fft.ifftn(-record.ky_norm * record.kx_norm * ux_k + (1.0 - record.ky_norm**2) * uy_k))
+ sensor_data.uy_split_s[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ elif (dim == 3):
+
+ # compute forward FFTs
+ ux_k = np.multiply(record.x_shift_neg, np.fft.fftn(ux_sgx), order='F')
+ uy_k = np.multiply(record.y_shift_neg, np.fft.fftn(uy_sgy), order='F')
+ uz_k = np.multiply(record.z_shift_neg, np.fft.fftn(uz_sgz), order='F')
+
+ # ux compressional
+ split_field = np.real(np.fft.ifftn(record.kx_norm**2 * ux_k +
+ record.kx_norm * record.ky_norm * uy_k +
+ record.kx_norm * record.kz_norm * uz_k))
+ sensor_data.ux_split_p[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # ux shear
+ split_field = np.real(np.fft.ifftn((1.0 - record.kx_norm**2) * ux_k -
+ record.kx_norm * record.ky_norm * uy_k -
+ record.kx_norm * record.kz_norm * uz_k))
+ sensor_data.ux_split_s[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # uy compressional
+ split_field = np.real(np.fft.ifftn(record.ky_norm * record.kx_norm * ux_k +
+ record.ky_norm**2 * uy_k +
+ record.ky_norm * record.kz_norm * uz_k))
+ sensor_data.uy_split_p[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # uy shear
+ split_field = np.real(np.fft.ifftn(- record.ky_norm * record.kx_norm * ux_k +
+ (1.0 - record.ky_norm**2) * uy_k -
+ record.ky_norm * record.kz_norm * uz_k))
+ sensor_data.uy_split_s[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # uz compressional
+ split_field = np.real(np.fft.ifftn(record.kz_norm * record.kx_norm * ux_k +
+ record.kz_norm * record.ky_norm * uy_k +
+ record.kz_norm**2 * uz_k))
+ sensor_data.uz_split_p[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # uz shear
+ split_field = np.real(np.fft.ifftn( -record.kz_norm * record.kx_norm * ux_k -
+ record.kz_norm * record.ky_norm * uy_k +
+ (1.0 - record.kz_norm**2) * uz_k))
+ sensor_data.uz_split_s[:, file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the maximum particle velocity
+ if flags.record_u_max:
+ if file_index == 0:
+ if (dim == 1):
+ sensor_data.ux_max = ux_sgx[sensor_mask_index]
+ elif (dim == 2):
+ sensor_data.ux_max = ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')]
+ sensor_data.uy_max = uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')]
+ elif (dim == 3):
+ sensor_data.ux_max = ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')]
+ sensor_data.uy_max = uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')]
+ sensor_data.uz_max = uz_sgz[np.unravel_index(np.squeeze(sensor_mask_index), uz_sgz.shape, order='F')]
+ else:
+ raise RuntimeError("Wrong dimensions")
+ else:
+ if (dim == 1):
+ sensor_data.ux_max = np.maximum(sensor_data.ux_max, ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')])
+ elif (dim == 2):
+ sensor_data.ux_max = np.maximum(sensor_data.ux_max, ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')])
+ sensor_data.uy_max = np.maximum(sensor_data.uy_max, uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')])
+ elif (dim == 3):
+ sensor_data.ux_max = np.maximum(sensor_data.ux_max, ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')])
+ sensor_data.uy_max = np.maximum(sensor_data.uy_max, uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')])
+ sensor_data.uz_max = np.maximum(sensor_data.uz_max, uz_sgz[np.unravel_index(np.squeeze(sensor_mask_index), uz_sgz.shape, order='F')])
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the minimum particle velocity
+ if flags.record_u_min:
+ if file_index == 0:
+ if (dim == 1):
+ sensor_data.ux_min = ux_sgx[sensor_mask_index]
+ elif (dim == 2):
+ sensor_data.ux_min = ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')]
+ sensor_data.uy_min = uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')]
+ elif (dim == 3):
+ sensor_data.ux_min = ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')]
+ sensor_data.uy_min = uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')]
+ sensor_data.uz_min = uz_sgz[np.unravel_index(np.squeeze(sensor_mask_index), uz_sgz.shape, order='F')]
+ else:
+ raise RuntimeError("Wrong dimensions")
+ else:
+ if (dim == 1):
+ sensor_data.ux_min = np.minimum(sensor_data.ux_min, ux_sgx[sensor_mask_index])
+ elif (dim == 2):
+ sensor_data.ux_min = np.minimum(sensor_data.ux_min, ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')])
+ sensor_data.uy_min = np.minimum(sensor_data.uy_min, uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')])
+ elif (dim == 3):
+ sensor_data.ux_min = np.minimum(sensor_data.ux_min, ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')])
+ sensor_data.uy_min = np.minimum(sensor_data.uy_min, uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')])
+ sensor_data.uz_min = np.minimum(sensor_data.uz_min, uz_sgz[np.unravel_index(np.squeeze(sensor_mask_index), uz_sgz.shape, order='F')])
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the rms particle velocity
+ if flags.record_u_rms:
+ if (dim == 1):
+ sensor_data.ux_rms = np.sqrt((sensor_data.ux_rms**2 * (file_index - 0) +
+ ux_sgx[sensor_mask_index]**2) / (file_index +1))
+ elif (dim == 2):
+ sensor_data.ux_rms = np.sqrt((sensor_data.ux_rms**2 * (file_index - 0) +
+ ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')]**2) / (file_index +1))
+ sensor_data.uy_rms = np.sqrt((sensor_data.uy_rms**2 * (file_index - 0) +
+ uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')]**2) / (file_index +1))
+ elif (dim == 3):
+ sensor_data.ux_rms = np.sqrt((sensor_data.ux_rms**2 * (file_index - 0) +
+ ux_sgx[np.unravel_index(np.squeeze(sensor_mask_index), ux_sgx.shape, order='F')]**2) / (file_index +1))
+ sensor_data.uy_rms = np.sqrt((sensor_data.uy_rms**2 * (file_index - 0) +
+ uy_sgy[np.unravel_index(np.squeeze(sensor_mask_index), uy_sgy.shape, order='F')]**2) / (file_index +1))
+ sensor_data.uz_rms = np.sqrt((sensor_data.uz_rms**2 * (file_index - 0) +
+ uz_sgz[np.unravel_index(np.squeeze(sensor_mask_index), uz_sgz.shape, order='F')]**2) / (file_index +1))
+
+
+ # =========================================================================
+ # CARTESIAN SENSOR MASK
+ # =========================================================================
+
+ elif flags.use_cuboid_corners:
+
+ n_cuboids: int = np.shape(record.cuboid_corners_list)[1]
+
+ # s_start: int = 0
+
+ # for each cuboid
+ for cuboid_index in np.arange(n_cuboids):
+
+ # get size of cuboid for indexing regions of computational grid
+ if dim == 1:
+ cuboid_size_x = [record.cuboid_corners_list[1, cuboid_index] - record.cuboid_corners_list[0, cuboid_index], 1]
+ elif dim == 2:
+ cuboid_size_x = [record.cuboid_corners_list[2, cuboid_index] - record.cuboid_corners_list[0, cuboid_index],
+ record.cuboid_corners_list[3, cuboid_index] - record.cuboid_corners_list[1, cuboid_index]]
+ elif dim == 3:
+ cuboid_size_x = [record.cuboid_corners_list[3, cuboid_index] + 1 - record.cuboid_corners_list[0, cuboid_index],
+ record.cuboid_corners_list[4, cuboid_index] + 1 - record.cuboid_corners_list[1, cuboid_index],
+ record.cuboid_corners_list[5, cuboid_index] + 1 - record.cuboid_corners_list[2, cuboid_index]]
+
+ # cuboid_num_points: int = np.prod(cuboid_size_x)
+
+ # s_end: int = s_start + cuboid_num_points
+
+ # sensor_mask_sub_index = sensor_mask_index[s_start:s_end]
+
+ # s_start = s_end
+
+ x_indices = np.arange(record.cuboid_corners_list[0, cuboid_index], record.cuboid_corners_list[3, cuboid_index]+1, dtype=int)
+ y_indices = np.arange(record.cuboid_corners_list[1, cuboid_index], record.cuboid_corners_list[4, cuboid_index]+1, dtype=int)
+ z_indices = np.arange(record.cuboid_corners_list[2, cuboid_index], record.cuboid_corners_list[5, cuboid_index]+1, dtype=int)
+
+ # Create a meshgrid
+ xx, yy, zz = np.meshgrid(x_indices, y_indices, z_indices, indexing='ij')
+
+ # Combine into a list of indices
+ cuboid_indices = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T
+
+ result = p[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+
+ # store the time history of the acoustic pressure
+ if (flags.record_p or flags.record_I or flags.record_I_avg):
+ if not flags.compute_directivity:
+ # Use the indices to index into p
+ sensor_data[cuboid_index].p[..., file_index] = result
+ else:
+ raise NotImplementedError('directivity not implemented at the moment')
+
+ # store the maximum acoustic pressure
+ if flags.record_p_max:
+ if file_index == 0:
+ sensor_data[cuboid_index].p_max = result
+ else:
+ sensor_data[cuboid_index].p_max = np.maximum(sensor_data[cuboid_index].p_max, result)
+
+ # store the minimum acoustic pressure
+ if flags.record_p_min:
+ if file_index == 0:
+ sensor_data[cuboid_index].p_min = result
+ else:
+ sensor_data[cuboid_index].p_min = np.maximum(sensor_data[cuboid_index].p_min, result)
+
+ # store the rms acoustic pressure
+ if flags.record_p_rms:
+ if file_index == 0:
+ sensor_data[cuboid_index].p_rms = result**2
+ else:
+ sensor_data[cuboid_index].p_rms = np.sqrt((sensor_data[cuboid_index].p_rms**2 * file_index + result**2) / (file_index + 1) )
+
+ # store the time history of the particle velocity on the staggered grid
+ if flags.record_u:
+ if (dim == 1):
+ sensor_data[cuboid_index].ux[..., file_index] = ux_sgx[cuboid_indices]
+ elif (dim == 2):
+ sensor_data[cuboid_index].ux[..., file_index] = ux_sgx[cuboid_indices]
+ sensor_data[cuboid_index].uy[..., file_index] = uy_sgy[cuboid_indices]
+ elif (dim == 3):
+ sensor_data[cuboid_index].ux[..., file_index] = ux_sgx[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+ sensor_data[cuboid_index].uy[..., file_index] = uy_sgy[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+ sensor_data[cuboid_index].uz[..., file_index] = uz_sgz[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the time history of the particle velocity
+ if flags.record_u_non_staggered or flags.record_I or flags.record_I_avg:
+ if (dim == 1):
+ sensor_data[cuboid_index].ux_non_staggered[..., file_index] = ux_shifted[cuboid_indices]
+ elif (dim == 2):
+ sensor_data[cuboid_index].ux_non_staggered[..., file_index] = ux_shifted[cuboid_indices]
+ sensor_data[cuboid_index].uy_non_staggered[..., file_index] = uy_shifted[cuboid_indices]
+ elif (dim == 3):
+ sensor_data[cuboid_index].ux_non_staggered[..., file_index] = ux_shifted[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+ sensor_data[cuboid_index].uy_non_staggered[..., file_index] = uy_shifted[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+ sensor_data[cuboid_index].uz_non_staggered[..., file_index] = uz_shifted[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the split components of the particle velocity
+ if flags.record_u_split_field:
+ if (dim == 2):
+
+ # compute forward FFTs
+ ux_k = record.x_shift_neg * np.fft.fftn(ux_sgx)
+ uy_k = record.y_shift_neg * np.fft.fftn(uy_sgy)
+
+ # ux compressional
+ split_field = np.real(np.fft.ifftn(record.kx_norm**2 * ux_k + record.kx_norm * record.ky_norm * uy_k))
+ sensor_data.ux_split_p[..., file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # ux shear
+ split_field = np.real(np.fft.ifftn((1.0 - record.kx_norm**2) * ux_k - record.kx_norm * record.ky_norm * uy_k))
+ sensor_data[cuboid_index].ux_split_s[..., file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # uy compressional
+ split_field = np.real(np.fft.ifftn(record.ky_norm * record.kx_norm * ux_k + record.ky_norm **2 * uy_k))
+ sensor_data[cuboid_index].uy_split_p[..., file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ # uy shear
+ split_field = np.real(np.fft.ifftn(-record.ky_norm * record.kx_norm * ux_k + (1.0 - record.ky_norm**2) * uy_k))
+ sensor_data[cuboid_index].uy_split_s[..., file_index] = split_field[np.unravel_index(np.squeeze(sensor_mask_index), split_field.shape, order='F')]
+
+ elif (dim == 3):
+
+ # compute forward FFTs
+ ux_k = np.multiply(record.x_shift_neg, np.fft.fftn(ux_sgx), order='F')
+ uy_k = np.multiply(record.y_shift_neg, np.fft.fftn(uy_sgy), order='F')
+ uz_k = np.multiply(record.z_shift_neg, np.fft.fftn(uz_sgz), order='F')
+
+ # ux compressional
+ split_field = np.real(np.fft.ifftn(record.kx_norm**2 * ux_k +
+ record.kx_norm * record.ky_norm * uy_k +
+ record.kx_norm * record.kz_norm * uz_k))
+ sensor_data[cuboid_index].ux_split_p[..., file_index] = split_field[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+ # ux shear
+ split_field = np.real(np.fft.ifftn((1.0 - record.kx_norm**2) * ux_k -
+ record.kx_norm * record.ky_norm * uy_k -
+ record.kx_norm * record.kz_norm * uz_k))
+ sensor_data[cuboid_index].ux_split_s[..., file_index] = split_field[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+
+ # uy compressional
+ split_field = np.real(np.fft.ifftn(record.ky_norm * record.kx_norm * ux_k +
+ record.ky_norm**2 * uy_k +
+ record.ky_norm * record.kz_norm * uz_k))
+ sensor_data[cuboid_index].uy_split_p[..., file_index] = split_field[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+
+ # uy shear
+ split_field = np.real(np.fft.ifftn(- record.ky_norm * record.kx_norm * ux_k +
+ (1.0 - record.ky_norm**2) * uy_k -
+ record.ky_norm * record.kz_norm * uz_k))
+ sensor_data[cuboid_index].uy_split_s[..., file_index] = split_field[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+
+ # uz compressional
+ split_field = np.real(np.fft.ifftn(record.kz_norm * record.kx_norm * ux_k +
+ record.kz_norm * record.ky_norm * uy_k +
+ record.kz_norm**2 * uz_k))
+ sensor_data[cuboid_index].uz_split_p[..., file_index] = split_field[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+
+ # uz shear
+ split_field = np.real(np.fft.ifftn( -record.kz_norm * record.kx_norm * ux_k -
+ record.kz_norm * record.ky_norm * uy_k +
+ (1.0 - record.kz_norm**2) * uz_k))
+ sensor_data[cuboid_index].uz_split_s[..., file_index] = split_field[cuboid_indices[:, 0], cuboid_indices[:, 1], cuboid_indices[:, 2]].reshape(cuboid_size_x)
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+
+
+
+
+
+
+
+
+ # =========================================================================
+ # CARTESIAN SENSOR MASK
+ # =========================================================================
+
+ # extract data from specified Cartesian coordinates using interpolation
+ # (record.tri and record.bc are the Delaunay triangulation and Barycentric coordinates
+ # returned by gridDataFast3D)
+ else:
+
+ # store the time history of the acoustic pressure
+ if flags.record_p or flags.record_I or flags.record_I_avg:
+ if dim == 1:
+ # i = np.argmin(np.abs(record.grid_x - record.sensor_x[0])).astype(int)
+ # j = np.argmin(np.abs(record.grid_x - record.sensor_x[1])).astype(int)
+ # print("THIS:", p.shape, file_index, i, j, record.sensor_x, record.sensor_x[0], record.sensor_x[1], record.grid_x[i], record.grid_x[j],
+ # p[i], p[j],
+ # record.grid_x[0], record.grid_x[-1], p.max())
+ sensor_data.p[:, file_index] = np.interp(np.squeeze(record.sensor_x), np.squeeze(record.grid_x), p)
+ else:
+ sensor_data.p[:, file_index] = np.sum(p[record.tri] * record.bc, axis=1)
+
+ # store the maximum acoustic pressure
+ if flags.record_p_max:
+ if dim == 1:
+ if file_index == 0:
+ sensor_data.p_max = np.interp(record.grid_x, p, record.sensor_x)
+ else:
+ sensor_data.p_max = np.maximum(sensor_data.p_max, np.interp(record.grid_x, p, record.sensor_x))
+ else:
+ if file_index == 0:
+ sensor_data.p_max = np.sum(p[record.tri] * record.bc, axis=1)
+ else:
+ sensor_data.p_max = np.maximum(sensor_data.p_max, np.sum(p[record.tri] * record.bc, axis=1))
+
+ # store the minimum acoustic pressure
+ if flags.record_p_min:
+ if dim == 1:
+ if file_index == 0:
+ sensor_data.p_min = np.interp(record.grid_x, p, record.sensor_x)
+ else:
+ sensor_data.p_min = np.minimum(sensor_data.p_min, np.interp(record.grid_x, p, record.sensor_x))
+ else:
+ if file_index == 0:
+ sensor_data.p_min = np.sum(p[record.tri] * record.bc, axis=1)
+ else:
+ sensor_data.p_min = np.minimum(sensor_data.p_min, np.sum(p[record.tri] * record.bc, axis=1))
+
+ # store the rms acoustic pressure
+ if flags.record_p_rms:
+ if dim == 1:
+ sensor_data.p_rms = np.sqrt((sensor_data.p_rms**2 * (file_index - 0) + (np.interp(record.grid_x, p, record.sensor_x))**2) / (file_index +1))
+ else:
+ sensor_data.p_rms[:] = np.sqrt((sensor_data.p_rms[:]**2 * (file_index - 0) +
+ (np.sum(p[record.tri] * record.bc, axis=1))**2) / (file_index +1))
+
+ # store the time history of the particle velocity on the staggered grid
+ if flags.record_u:
+ if (dim == 1):
+ sensor_data.ux[:, file_index] = np.interp(record.grid_x, ux_sgx, record.sensor_x)
+ elif (dim == 2):
+ sensor_data.ux[:, file_index] = np.sum(ux_sgx[record.tri] * record.bc, axis=1)
+ sensor_data.uy[:, file_index] = np.sum(uy_sgy[record.tri] * record.bc, axis=1)
+ elif (dim == 3):
+ sensor_data.ux[:, file_index] = np.sum(ux_sgx[record.tri] * record.bc, axis=1)
+ sensor_data.uy[:, file_index] = np.sum(uy_sgy[record.tri] * record.bc, axis=1)
+ sensor_data.uz[:, file_index] = np.sum(uz_sgz[record.tri] * record.bc, axis=1)
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the time history of the particle velocity
+ if flags.record_u_non_staggered or flags.record_I or flags.record_I_avg:
+ if (dim == 1):
+ sensor_data.ux_non_staggered[:, file_index] = np.interp(record.grid_x, ux_shifted, record.sensor_x)
+ elif (dim == 2):
+ sensor_data.ux_non_staggered[:, file_index] = np.sum(ux_shifted[record.tri] * record.bc, axis=1)
+ sensor_data.uy_non_staggered[:, file_index] = np.sum(uy_shifted[record.tri] * record.bc, axis=1)
+ elif (dim == 3):
+ sensor_data.ux_non_staggered[:, file_index] = np.sum(ux_shifted[record.tri] * record.bc, axis=1)
+ sensor_data.uy_non_staggered[:, file_index] = np.sum(uy_shifted[record.tri] * record.bc, axis=1)
+ sensor_data.uz_non_staggered[:, file_index] = np.sum(uz_shifted[record.tri] * record.bc, axis=1)
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the maximum particle velocity
+ if flags.record_u_max:
+ if file_index == 0:
+ if (dim == 1):
+ sensor_data.ux_max = np.interp(record.grid_x, ux_sgx, record.sensor_x)
+ elif (dim == 2):
+ sensor_data.ux_max = np.sum(ux_sgx[record.tri] * record.bc, axis=1)
+ sensor_data.uy_max = np.sum(uy_sgy[record.tri] * record.bc, axis=1)
+ elif (dim == 3):
+ sensor_data.ux_max = np.sum(ux_sgx[record.tri] * record.bc, axis=1)
+ sensor_data.uy_max = np.sum(uy_sgy[record.tri] * record.bc, axis=1)
+ sensor_data.uz_max = np.sum(uz_sgz[record.tri] * record.bc, axis=1)
+ else:
+ raise RuntimeError("Wrong dimensions")
+ else:
+ if (dim == 1):
+ sensor_data.ux_max = np.maximum(sensor_data.ux_max, np.interp(record.grid_x, ux_sgx, record.sensor_x))
+ elif (dim == 2):
+ sensor_data.ux_max = np.maximum(sensor_data.ux_max, np.sum(ux_sgx[record.tri] * record.bc, axis=1))
+ sensor_data.uy_max = np.maximum(sensor_data.uy_max, np.sum(uy_sgy[record.tri] * record.bc, axis=1))
+ elif (dim == 3):
+ sensor_data.ux_max = np.maximum(sensor_data.ux_max, np.sum(ux_sgx[record.tri] * record.bc, axis=1))
+ sensor_data.uy_max = np.maximum(sensor_data.uy_max, np.sum(uy_sgy[record.tri] * record.bc, axis=1))
+ sensor_data.uz_max = np.maximum(sensor_data.uz_max, np.sum(uz_sgz[record.tri] * record.bc, axis=1))
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the minimum particle velocity
+ if flags.record_u_min:
+ if file_index == 0:
+ if (dim == 1):
+ sensor_data.ux_min = np.interp(record.grid_x, ux_sgx, record.sensor_x)
+ elif (dim == 2):
+ sensor_data.ux_min = np.sum(ux_sgx[record.tri] * record.bc, axis=1)
+ sensor_data.uy_min = np.sum(uy_sgy[record.tri] * record.bc, axis=1)
+ elif (dim == 3):
+ sensor_data.ux_min = np.sum(ux_sgx[record.tri] * record.bc, axis=1)
+ sensor_data.uy_min = np.sum(uy_sgy[record.tri] * record.bc, axis=1)
+ sensor_data.uz_min = np.sum(uz_sgz[record.tri] * record.bc, axis=1)
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ else:
+ if (dim == 1):
+ sensor_data.ux_min = np.minimum(sensor_data.ux_min, np.interp(record.grid_x, ux_sgx, record.sensor_x))
+ elif (dim == 2):
+ sensor_data.ux_min = np.minimum(sensor_data.ux_min, np.sum(ux_sgx[record.tri] * record.bc, axis=1))
+ sensor_data.uy_min = np.minimum(sensor_data.uy_min, np.sum(uy_sgy[record.tri] * record.bc, axis=1))
+ elif (dim == 3):
+ sensor_data.ux_min = np.minimum(sensor_data.ux_min, np.sum(ux_sgx[record.tri] * record.bc, axis=1))
+ sensor_data.uy_min = np.minimum(sensor_data.uy_min, np.sum(uy_sgy[record.tri] * record.bc, axis=1))
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the rms particle velocity
+ if flags.record_u_rms:
+ if (dim == 1):
+ sensor_data.ux_rms = np.sqrt((sensor_data.ux_rms**2 * (file_index - 0) + (np.interp(record.grid_x, ux_sgx, record.sensor_x))**2) / (file_index +1))
+ elif (dim == 2):
+ sensor_data.ux_rms[:] = np.sqrt((sensor_data.ux_rms[:]**2 * (file_index - 0) + (np.sum(ux_sgx[record.tri] * record.bc, axis=1))**2) / (file_index +1))
+ sensor_data.uy_rms[:] = np.sqrt((sensor_data.uy_rms[:]**2 * (file_index - 0) + (np.sum(uy_sgy[record.tri] * record.bc, axis=1))**2) / (file_index +1))
+ elif (dim == 3):
+ sensor_data.ux_rms[:] = np.sqrt((sensor_data.ux_rms[:]**2 * (file_index - 0) + (np.sum(ux_sgx[record.tri] * record.bc, axis=1))**2) / (file_index +1))
+ sensor_data.uy_rms[:] = np.sqrt((sensor_data.uy_rms[:]**2 * (file_index - 0) + (np.sum(uy_sgy[record.tri] * record.bc, axis=1))**2) / (file_index +1))
+ sensor_data.uz_rms[:] = np.sqrt((sensor_data.uz_rms[:]**2 * (file_index - 0) + (np.sum(uz_sgz[record.tri] * record.bc, axis=1))**2) / (file_index +1))
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # =========================================================================
+ # RECORDED VARIABLES OVER ENTIRE GRID
+ # =========================================================================
+
+ # store the maximum acoustic pressure over all the grid elements
+ if flags.record_p_max_all and (not flags.use_cuboid_corners):
+ if (dim == 1):
+ if file_index == 0:
+ sensor_data.p_max_all = p[record.x1_inside:record.x2_inside]
+ else:
+ sensor_data.p_max_all = np.maximum(sensor_data.p_max_all, p[record.x1_inside:record.x2_inside])
+
+ elif (dim == 2):
+ if file_index == 0:
+ sensor_data.p_max_all = p[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside]
+ else:
+ sensor_data.p_max_all = np.maximum(sensor_data.p_max_all, p[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside])
+
+ elif (dim == 3):
+ if file_index == 0:
+ sensor_data.p_max_all = p[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ else:
+ sensor_data.p_max_all = np.maximum(sensor_data.p_max_all,
+ p[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside])
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the minimum acoustic pressure over all the grid elements
+ if flags.record_p_min_all and (not flags.use_cuboid_corners):
+ if (dim == 1):
+ if file_index == 0:
+ sensor_data.p_min_all = p[record.x1_inside:record.x2_inside]
+ else:
+ sensor_data.p_min_all = np.minimum(sensor_data.p_min_all, p[record.x1_inside:record.x2_inside])
+
+ elif (dim == 2):
+ if file_index == 0:
+ sensor_data.p_min_all = p[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside]
+ else:
+ sensor_data.p_min_all = np.minimum(sensor_data.p_min_all, p[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside])
+
+ elif (dim == 3):
+ if file_index == 0:
+ sensor_data.p_min_all = p[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ else:
+ sensor_data.p_min_all = np.minimum(sensor_data.p_min_all,
+ p[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside])
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the maximum particle velocity over all the grid elements
+ if flags.record_u_max_all and (not flags.use_cuboid_corners):
+ if (dim == 1):
+ if file_index == 0:
+ sensor_data.ux_max_all = ux_sgx[record.x1_inside:record.x2_inside]
+ else:
+ sensor_data.ux_max_all = np.maximum(sensor_data.ux_max_all, ux_sgx[record.x1_inside:record.x2_inside])
+
+ elif (dim == 2):
+ if file_index == 0:
+ sensor_data.ux_max_all = ux_sgx[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside]
+ sensor_data.uy_max_all = uy_sgy[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside]
+ else:
+ sensor_data.ux_max_all = np.maximum(sensor_data.ux_max_all,
+ ux_sgx[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside])
+ sensor_data.uy_max_all = np.maximum(sensor_data.uy_max_all,
+ uy_sgy[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside])
+
+ elif (dim == 3):
+ if file_index == 0:
+ sensor_data.ux_max_all = ux_sgx[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ sensor_data.uy_max_all = uy_sgy[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ sensor_data.uz_max_all = uz_sgz[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ else:
+ sensor_data.ux_max_all = np.maximum(sensor_data.ux_max_all,
+ ux_sgx[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside])
+ sensor_data.uy_max_all = np.maximum(sensor_data.uy_max_all,
+ uy_sgy[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside])
+ sensor_data.uz_max_all = np.maximum(sensor_data.uz_max_all,
+ uz_sgz[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside])
+
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ # store the minimum particle velocity over all the grid elements
+ if flags.record_u_min_all and (not flags.use_cuboid_corners):
+ if (dim == 1):
+ if file_index == 0:
+ sensor_data.ux_min_all = ux_sgx[record.x1_inside:record.x2_inside]
+ else:
+ sensor_data.ux_min_all = np.minimum(sensor_data.ux_min_all, ux_sgx[record.x1_inside:record.x2_inside])
+
+ elif (dim == 2):
+ if file_index == 0:
+ sensor_data.ux_min_all = ux_sgx[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside]
+ sensor_data.uy_min_all = uy_sgy[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside]
+ else:
+ sensor_data.ux_min_all = np.minimum(sensor_data.ux_min_all,
+ ux_sgx[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside])
+ sensor_data.uy_min_all = np.minimum(sensor_data.uy_min_all,
+ uy_sgy[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside])
+
+ elif (dim == 3):
+ if file_index == 0:
+ sensor_data.ux_min_all = ux_sgx[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ sensor_data.uy_min_all = uy_sgy[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ sensor_data.uz_min_all = uz_sgz[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ else:
+ sensor_data.ux_min_all = np.minimum(sensor_data.ux_min_all,
+ ux_sgx[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside])
+ sensor_data.uy_min_all = np.minimum(sensor_data.uy_min_all,
+ uy_sgy[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside])
+ sensor_data.uz_min_all = np.minimum(sensor_data.uz_min_all,
+ uz_sgz[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside])
+ else:
+ raise RuntimeError("Wrong dimensions")
+
+ return sensor_data
\ No newline at end of file
diff --git a/kwave/kWaveSimulation_helper/reorder_cuboid_corners.py b/kwave/kWaveSimulation_helper/reorder_cuboid_corners.py
new file mode 100644
index 000000000..456d65fff
--- /dev/null
+++ b/kwave/kWaveSimulation_helper/reorder_cuboid_corners.py
@@ -0,0 +1,247 @@
+import numpy as np
+
+from kwave.utils.dotdictionary import dotdict
+
+def reorder_cuboid_corners(kgrid, record, sensor_data, time_info, flags, verbose: bool = False):
+
+ """DESCRIPTION:
+ Method to reassign the sensor data belonging to each set of cuboid corners
+ from the indexed sensor mask data.
+
+ ABOUT:
+ author - Bradley Treeby
+ date - 8th July 2014
+ last update - 15th May 2018
+
+ This function is part of the k-Wave Toolbox (http://www.k-wave.org)
+ Copyright (C) 2014-2018 Bradley Treeby
+
+ This file is part of k-Wave. k-Wave is free software: you can
+ redistribute it and/or modify it under the terms of the GNU Lesser
+ General Public License as published by the Free Software Foundation,
+ either version 3 of the License, or (at your option) any later version.
+
+ k-Wave is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with k-Wave. If not, see .
+ """
+
+ # update command line status
+ if verbose:
+ print(' reordering cuboid corners data...', len(sensor_data))
+ #print(sensor_data)
+
+ def ensure_list(item):
+ if not isinstance(item, list):
+ print("return as list")
+ return [item]
+ print("is list apparently")
+ return item
+
+ # set cuboid index variable
+ cuboid_start_pos: int = 0
+
+ n_cuboids: int = np.shape(record.cuboid_corners_list)[1]
+
+ print(n_cuboids, np.shape(record.cuboid_corners_list))
+ if n_cuboids > 1:
+ sensor_data = ensure_list(sensor_data)
+
+ #print(np.shape(np.asarray(sensor_data)))
+ print(np.shape(sensor_data[0].p))
+
+ # create list of cuboid corners
+ sensor_data_temp = []
+
+ # set number of time points from dotdict container
+ if time_info.stream_to_disk:
+ cuboid_num_time_points = time_info.num_stream_time_points
+ else:
+ cuboid_num_time_points = time_info.num_recorded_time_points
+
+ # loop through cuboid corners and for each recorded variable, reshape to
+ # [X, Y, Z, Y] or [X, Y, Z] instead of [sensor_index, T] or [sensor_index]
+ for cuboid_index in np.arange(n_cuboids):
+
+ # get size of cuboid
+ if kgrid.dim == 1:
+ cuboid_size_x = [record.cuboid_corners_list[1, cuboid_index] - record.cuboid_corners_list[0, cuboid_index], 1]
+ cuboid_size_xt = [cuboid_size_x[0], cuboid_num_time_points]
+ elif kgrid.dim == 2:
+ cuboid_size_x = [record.cuboid_corners_list[2, cuboid_index] - record.cuboid_corners_list[0, cuboid_index],
+ record.cuboid_corners_list[3, cuboid_index] - record.cuboid_corners_list[1, cuboid_index]]
+ cuboid_size_xt = [cuboid_size_x, cuboid_num_time_points]
+ elif kgrid.dim == 3:
+ cuboid_size_x = [record.cuboid_corners_list[3, cuboid_index] - record.cuboid_corners_list[0, cuboid_index],
+ record.cuboid_corners_list[4, cuboid_index] - record.cuboid_corners_list[1, cuboid_index],
+ record.cuboid_corners_list[5, cuboid_index] - record.cuboid_corners_list[2, cuboid_index]]
+ cuboid_size_xt = [cuboid_size_x, cuboid_num_time_points]
+
+ # set index and size variables
+ cuboid_num_points = np.prod(cuboid_size_x)
+
+ # append empty dictionary into list
+ sensor_data_temp.append(dotdict())
+
+ if flags.record_p:
+ sensor_data_temp[cuboid_index].p = np.reshape(
+ sensor_data[cuboid_index].p[cuboid_start_pos:cuboid_start_pos + cuboid_num_points, :], cuboid_size_xt)
+
+ if flags.record_p_max:
+ sensor_data_temp[cuboid_index].p_max = np.reshape(
+ sensor_data[cuboid_index].p_max[cuboid_start_pos:cuboid_start_pos + cuboid_num_points], cuboid_size_x)
+
+ if flags.record_p_min:
+ sensor_data_temp[cuboid_index].p_min = np.reshape(
+ sensor_data[cuboid_index].p_min[cuboid_start_pos:cuboid_start_pos + cuboid_num_points], cuboid_size_x)
+
+ if flags.record_p_rms:
+ sensor_data_temp[cuboid_index].p_rms = np.reshape(
+ sensor_data[cuboid_index].p_rms[cuboid_start_pos:cuboid_start_pos + cuboid_num_points], cuboid_size_x)
+
+ if flags.record_u:
+ # x-dimension
+ sensor_data_temp[cuboid_index].ux = np.reshape(
+ sensor_data[cuboid_index].ux[cuboid_start_pos:cuboid_start_pos + cuboid_num_points, :], cuboid_size_xt)
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[cuboid_index].uy = np.reshape(
+ sensor_data[cuboid_index].uy[cuboid_start_pos:cuboid_start_pos + cuboid_num_points, :], cuboid_size_xt)
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[cuboid_index].uz = np.reshape(
+ sensor_data[cuboid_index].uz[cuboid_start_pos:cuboid_start_pos + cuboid_num_points, :], cuboid_size_xt)
+
+ if flags.record_u_non_staggered:
+ # x-dimension
+ sensor_data_temp[cuboid_index].ux_non_staggered = np.reshape(
+ sensor_data[cuboid_index].ux_non_staggered[cuboid_start_pos:cuboid_start_pos + cuboid_num_points, :], cuboid_size_xt)
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[cuboid_index].uy_non_staggered = np.reshape(
+ sensor_data[cuboid_index].uy_non_staggered[cuboid_start_pos:cuboid_start_pos + cuboid_num_points, :], cuboid_size_xt)
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[cuboid_index].uz_non_staggered = np.reshape(
+ sensor_data[cuboid_index].uz_non_staggered[cuboid_start_pos:cuboid_start_pos + cuboid_num_points, :], cuboid_size_xt)
+
+ if flags.record_u_max:
+ # x-dimension
+ sensor_data_temp[cuboid_index].ux_max = np.reshape(
+ sensor_data[cuboid_index].ux_max[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[cuboid_index].uy_max = np.reshape(
+ sensor_data[cuboid_index].uy_max[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[cuboid_index].uz_max = np.reshape(
+ sensor_data[cuboid_index].uz_max[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+
+ if flags.record_u_min:
+ # x-dimension
+ sensor_data_temp[cuboid_index].ux_min = np.reshape(
+ sensor_data[cuboid_index].ux_min[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[cuboid_index].uy_min = np.reshape(
+ sensor_data[cuboid_index].uy_min[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[cuboid_index].uz_min = np.reshape(
+ sensor_data[cuboid_index].uz_min[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+
+ if flags.record_u_rms:
+ # x-dimension
+ sensor_data_temp[cuboid_index].ux_rms = np.reshape(
+ sensor_data[cuboid_index].ux_rms[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[cuboid_index].uy_rms = np.reshape(
+ sensor_data[cuboid_index].uy_rms[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[cuboid_index].uz_rms = np.reshape(
+ sensor_data[cuboid_index].uz_rms[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+
+ if flags.record_I:
+ # x-dimension
+ sensor_data_temp[cuboid_index].Ix = np.reshape(
+ sensor_data[cuboid_index].Ix[cuboid_start_pos:cuboid_start_pos + cuboid_num_points , :], cuboid_size_xt)
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[cuboid_index].Iy = np.reshape(
+ sensor_data[cuboid_index].Iy[cuboid_start_pos:cuboid_start_pos + cuboid_num_points , :], cuboid_size_xt)
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[cuboid_index].Iz = np.reshape(
+ sensor_data[cuboid_index].Iz[cuboid_start_pos:cuboid_start_pos + cuboid_num_points , :], cuboid_size_xt)
+
+ if flags.record_I_avg:
+ # x-dimension
+ sensor_data_temp[cuboid_index].Ix_avg = np.reshape(
+ sensor_data[cuboid_index].Ix_avg[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[cuboid_index].Iy_avg = np.reshape(
+ sensor_data[cuboid_index].Iy_avg[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[cuboid_index].Iz_avg = np.reshape(
+ sensor_data[cuboid_index].Iz_avg[cuboid_start_pos:cuboid_start_pos + cuboid_num_points ], cuboid_size_x)
+
+ # update cuboid index variable
+ cuboid_start_pos = cuboid_start_pos + cuboid_num_points
+
+
+ if any([flags.record_p_min_all, flags.record_p_max_all, flags.record_u_max_all, flags.record_u_min_all]):
+ last_cuboid: int = n_cuboids + 1
+
+ # assign max and final variables
+ if flags.record_p_final:
+ sensor_data_temp[last_cuboid].p_final = sensor_data.p_final
+
+ if flags.record_u_final:
+ # x-dimension
+ sensor_data_temp[last_cuboid].ux_final = sensor_data.ux_final
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[last_cuboid].uy_final = sensor_data.uy_final
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[last_cuboid].uz_final = sensor_data.uz_final
+
+ if flags.record_p_max_all:
+ sensor_data_temp[last_cuboid].p_max_all = sensor_data.p_max_all
+
+ if flags.record_p_min_all:
+ sensor_data_temp[last_cuboid].p_min_all = sensor_data.p_min_all
+
+ if flags.record_u_max_all:
+ # x-dimension
+ sensor_data_temp[last_cuboid].ux_max_all = sensor_data.ux_max_all
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[last_cuboid].uy_max_all = sensor_data.uy_max_all
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[last_cuboid].uz_max_all = sensor_data.uz_max_all
+
+ if flags.record_u_min_all:
+ # x-dimension
+ sensor_data_temp[last_cuboid].ux_min_all = sensor_data.ux_min_all
+ # y-dimension if 2D or 3D
+ if kgrid.dim > 1:
+ sensor_data_temp[last_cuboid].uy_min_all = sensor_data.uy_min_all
+ # z-dimension if 3D
+ if kgrid.dim > 2:
+ sensor_data_temp[last_cuboid].uz_min_all = sensor_data.uz_min_all
+
+ # assign new sensor data to old
+ sensor_data = sensor_data_temp
+
+ return sensor_data
diff --git a/kwave/kWaveSimulation_helper/save_intensity.py b/kwave/kWaveSimulation_helper/save_intensity.py
new file mode 100644
index 000000000..98ee3d852
--- /dev/null
+++ b/kwave/kWaveSimulation_helper/save_intensity.py
@@ -0,0 +1,140 @@
+import numpy as np
+from kwave.utils.math import fourier_shift
+
+def save_intensity(kgrid, sensor_data, save_intensity_options):
+ """
+ save_intensity is a method to calculate the acoustic intensity from the time
+ varying acoustic pressure and particle velocity recorded during the simulation.
+ The particle velocity is first temporally shifted forwards by dt/2 using a
+ Fourier interpolant so both variables are on the regular (non-staggered) grid.
+
+ This function is called before the binary sensor data is reordered
+ for cuboid corners, so it works for both types of sensor mask.
+
+ If using cuboid corners the sensor data may be given as a structure
+ array, i.e., sensor_data(n).ux_non_staggered. In this case, the
+ calculation of intensity is applied to each cuboid sensor mask separately.
+
+ ABOUT:
+ author - Bradley Treeby
+ date - 4th September 2013
+ last update - 15th May 2018
+
+ This function is part of the k-Wave Toolbox (http://www.k-wave.org)
+ Copyright (C) 2013-2018 Bradley Treeby
+
+ This file is part of k-Wave. k-Wave is free software: you can
+ redistribute it and/or modify it under the terms of the GNU Lesser
+ General Public License as published by the Free Software Foundation,
+ either version 3 of the License, or (at your option) any later version.
+
+ k-Wave is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with k-Wave. If not, see .
+ """
+
+ shift = 0.5
+
+ def ensure_list(item):
+ if not isinstance(item, list):
+ return [item]
+ return item
+
+ # loop through the number of sensor masks (this can be > 1 if using cuboid
+ # corners)
+ if save_intensity_options.use_cuboid_corners:
+
+ # if not a list, i.e. only one set of cuboid corners, make a list.
+
+ sensor_data = ensure_list(sensor_data)
+
+ l_sensor_data: int = len(sensor_data)
+
+ # this states that if _all data is recorded, which is stored as a separate entry in the list,
+ # then the number of entries in which the intensity is computed should be reduced by one.
+ if (l_sensor_data > 1) and (sensor_data[-1].ux_non_staggered is None):
+ l_sensor_data = l_sensor_data - int(1)
+
+ for sensor_mask_index in np.arange(l_sensor_data):
+
+ print(sensor_mask_index)
+
+ # shift the recorded particle velocity to the regular (non-staggered)
+ # temporal grid and then compute the time varying intensity
+ ux_sgt = fourier_shift(sensor_data[sensor_mask_index].ux_non_staggered, shift=shift)
+ sensor_data[sensor_mask_index].Ix = np.multiply(sensor_data[sensor_mask_index].p, ux_sgt, order='F')
+ if kgrid.dim > 1:
+ uy_sgt = fourier_shift(sensor_data[sensor_mask_index].uy_non_staggered, shift=shift)
+ sensor_data[sensor_mask_index].Iy = np.multiply(sensor_data[sensor_mask_index].p, uy_sgt, order='F')
+ if kgrid.dim > 2:
+ uz_sgt = fourier_shift(sensor_data[sensor_mask_index].uz_non_staggered, shift=shift)
+ sensor_data[sensor_mask_index].Iz = np.multiply(sensor_data[sensor_mask_index].p, uz_sgt, order='F')
+
+ # calculate the time average of the intensity if required using the last
+ # dimension (this works for both linear and cuboid sensor masks)
+ if save_intensity_options.record_I_avg:
+ sensor_data[sensor_mask_index].Ix_avg = np.mean(sensor_data[sensor_mask_index].Ix,
+ axis=np.ndim(sensor_data[sensor_mask_index].Ix) - 1)
+ if kgrid.dim > 1:
+ sensor_data[sensor_mask_index].Iy_avg = np.mean(sensor_data[sensor_mask_index].Iy,
+ axis=np.ndim(sensor_data[sensor_mask_index].Iy) - 1)
+ if kgrid.dim > 2:
+ sensor_data[sensor_mask_index].Iz_avg = np.mean(sensor_data[sensor_mask_index].Iz,
+ axis=np.ndim(sensor_data[sensor_mask_index].Iz) - 1)
+ else:
+
+ # shift the recorded particle velocity to the regular (non-staggered)
+ # temporal grid and then compute the time varying intensity
+ ux_sgt = fourier_shift(sensor_data.ux_non_staggered, shift=shift)
+ sensor_data.Ix = np.multiply(sensor_data.p, ux_sgt, order='F')
+ if kgrid.dim > 1:
+ uy_sgt = fourier_shift(sensor_data.uy_non_staggered, shift=shift)
+ sensor_data.Iy = np.multiply(sensor_data.p, uy_sgt, order='F')
+ if kgrid.dim > 2:
+ uz_sgt = fourier_shift(sensor_data.uz_non_staggered, shift=shift)
+ sensor_data.Iz = np.multiply(sensor_data.p, uz_sgt, order='F')
+
+ # calculate the time average of the intensity if required using the last
+ # dimension (this works for both linear and cuboid sensor masks)
+ if save_intensity_options.record_I_avg:
+ sensor_data.Ix_avg = np.mean(sensor_data.Ix, axis=np.ndim(sensor_data.Ix) - 1)
+ if kgrid.dim > 1:
+ sensor_data.Iy_avg = np.mean(sensor_data.Iy, axis=np.ndim(sensor_data.Iy) - 1)
+ if kgrid.dim > 2:
+ sensor_data.Iz_avg = np.mean(sensor_data.Iz, axis=np.ndim(sensor_data.Iz) - 1)
+
+ # # remove the non staggered particle velocity variables if not required
+ # if not save_intensity_options.record_u_non_staggered:
+ # if kgrid.dim == 1:
+ # del sensor_data.ux_non_staggered
+ # elif kgrid.dim == 2:
+ # del sensor_data.ux_non_staggered
+ # del sensor_data.uy_non_staggered
+ # elif kgrid.dim == 3:
+ # del sensor_data.ux_non_staggered
+ # del sensor_data.uy_non_staggered
+ # del sensor_data.uz_non_staggered
+
+ # # remove the time varying intensity if not required
+ # if not save_intensity_options.record_I:
+ # if kgrid.dim == 1:
+ # del sensor_data.Ix
+ # elif kgrid.dim == 2:
+ # del sensor_data.Ix
+ # del sensor_data.Iy
+ # elif kgrid.dim == 3:
+ # del sensor_data.Ix
+ # del sensor_data.Iy
+ # del sensor_data.Iz
+
+ # # remove the time varying pressure if not required
+ # if not save_intensity_options.record_p:
+ # del sensor_data.Iy
+
+ sensor_data = sensor_data[0] if len(sensor_data) == 1 else sensor_data
+
+ return sensor_data
diff --git a/kwave/kWaveSimulation_helper/save_to_disk_func.py b/kwave/kWaveSimulation_helper/save_to_disk_func.py
index 30f901a9c..e2b760f21 100644
--- a/kwave/kWaveSimulation_helper/save_to_disk_func.py
+++ b/kwave/kWaveSimulation_helper/save_to_disk_func.py
@@ -44,6 +44,7 @@ def save_to_disk_func(
integer_variables.pml_z_size = 0
grab_medium_props(integer_variables, float_variables, medium, flags.elastic_code)
+
grab_source_props(
integer_variables,
float_variables,
@@ -55,7 +56,8 @@ def save_to_disk_func(
values.delay_mask,
)
- grab_sensor_props(integer_variables, kgrid.dim, values.sensor_mask_index, values.record.cuboid_corners_list)
+ grab_sensor_props(integer_variables, kgrid.dim, values.sensor_mask_index, values.record.cuboid_corners_list, flags)
+
grab_nonuniform_grid_props(float_variables, kgrid, flags.nonuniform_grid)
# =========================================================================
@@ -63,6 +65,7 @@ def save_to_disk_func(
# =========================================================================
remove_z_dimension(float_variables, kgrid.dim)
+
save_file(opt.input_filename, integer_variables, float_variables, opt.hdf_compression_level, auto_chunk=auto_chunk)
# update command line status
@@ -375,13 +378,17 @@ def grab_time_varying_source_props(integer_variables, float_variables, source, t
float_variables.p0_source_input = source.p0
-def grab_sensor_props(integer_variables, kgrid_dim, sensor_mask_index, cuboid_corners_list):
+def grab_sensor_props(integer_variables, kgrid_dim, sensor_mask_index, cuboid_corners_list, flags):
# =========================================================================
# SENSOR VARIABLES
# =========================================================================
+ # print("in grab sensor props", integer_variables.sensor_mask_type, flags.cuboid_corners,
+ # integer_variables.sensor_mask_type == 0, integer_variables.sensor_mask_type == 1)
+
if integer_variables.sensor_mask_type == 0:
# mask is defined as a list of grid indices
+ # print(sensor_mask_index)
integer_variables.sensor_mask_index = sensor_mask_index
elif integer_variables.sensor_mask_type == 1:
@@ -498,6 +505,10 @@ def save_h5_file(filepath, integer_variables, float_variables, hdf_compression_l
# (long in C++), then add to HDF5 file
for key, value in integer_variables.items():
# cast matrix to 64-bit unsigned integer
+ # print(key, value is not None)
+ if (value is None):
+ print("*********", key, "*********")
+ #else:
value = np.array(value, dtype=np.uint64)
write_matrix(filepath, value, key, hdf_compression_level, auto_chunk)
del value
diff --git a/kwave/kWaveSimulation_helper/scale_source_terms_func.py b/kwave/kWaveSimulation_helper/scale_source_terms_func.py
index e90c5b631..9a2a3246c 100644
--- a/kwave/kWaveSimulation_helper/scale_source_terms_func.py
+++ b/kwave/kWaveSimulation_helper/scale_source_terms_func.py
@@ -1,5 +1,6 @@
import logging
-import math
+
+from copy import deepcopy
import numpy as np
@@ -8,9 +9,11 @@
from kwave.utils.dotdictionary import dotdict
-def scale_source_terms_func(
- c0, dt, kgrid: kWaveGrid, source, p_source_pos_index, s_source_pos_index, u_source_pos_index, transducer_input_signal, flags: dotdict
-):
+def scale_source_terms_func(c0, dt, kgrid: kWaveGrid, source,
+ p_source_pos_index,
+ s_source_pos_index,
+ u_source_pos_index,
+ transducer_input_signal, flags: dotdict):
"""
Subscript for the first-order k-Wave simulation functions to scale source terms to the correct units.
Args:
@@ -54,7 +57,9 @@ def scale_source_terms_func(
# =========================================================================
# TRANSDUCER SOURCE
# =========================================================================
+
transducer_input_signal = scale_transducer_source(flags.transducer_source, transducer_input_signal, c0, dt, dx, u_source_pos_index)
+
return transducer_input_signal
@@ -176,14 +181,14 @@ def scale_pressure_source_nonuniform_grid(source_p, kgrid, c0, N, dt, p_source_p
def scale_pressure_source_uniform_grid(source_p, c0, N, dx, dt, p_source_pos_index):
+
if c0.size == 1:
- # compute the scale parameter based on the homogeneous
- # sound speed
+ # compute the scale parameter based on the homogeneous sound speed
source_p = source_p * (2 * dt / (N * c0 * dx))
else:
- # compute the scale parameter seperately for each source
- # position based on the sound speed at that position
+ # compute the scale parameter seperately for each source position based on the
+ # sound speed at that position
ind = range(source_p[:, 0].size)
mask = p_source_pos_index.flatten("F")[ind]
scale = (2.0 * dt) / (N * np.expand_dims(c0.ravel(order="F")[mask.ravel(order="F")], axis=-1) * dx)
@@ -209,30 +214,49 @@ def scale_stress_sources(source, c0, flags, dt, dx, N, s_source_pos_index):
Returns:
"""
- source.sxx = scale_stress_source(source, c0, flags.source_sxx, flags.source_p0, source.sxx, dt, N, dx, s_source_pos_index)
- source.syy = scale_stress_source(source, c0, flags.source_syy, flags.source_p0, source.syy, dt, N, dx, s_source_pos_index)
- source.szz = scale_stress_source(source, c0, flags.source_szz, flags.source_p0, source.szz, dt, N, dx, s_source_pos_index)
- source.sxy = scale_stress_source(source, c0, flags.source_sxy, True, source.sxy, dt, N, dx, s_source_pos_index)
- source.sxz = scale_stress_source(source, c0, flags.source_sxz, True, source.sxz, dt, N, dx, s_source_pos_index)
- source.syz = scale_stress_source(source, c0, flags.source_syz, True, source.syz, dt, N, dx, s_source_pos_index)
-
-def scale_stress_source(source, c0, is_source_exists, is_p0_exists, source_val, dt, N, dx, s_source_pos_index):
+ if flags.source_sxx:
+ print("scale source.sxx")
+ source.sxx = scale_stress_source(source, c0, flags.source_sxx, flags.source_p0, deepcopy(source.sxx), dt, N, dx, s_source_pos_index)
+ if flags.source_syy:
+ print("scale source.syy")
+ source.syy = scale_stress_source(source, c0, flags.source_syy, flags.source_p0, deepcopy(source.syy), dt, N, dx, s_source_pos_index)
+ if flags.source_szz:
+ print("scale source.szz")
+ source.szz = scale_stress_source(source, c0, flags.source_szz, flags.source_p0, deepcopy(source.szz), dt, N, dx, s_source_pos_index)
+ if flags.source_sxy:
+ source.sxy = scale_stress_source(source, c0, flags.source_sxy, flags.source_p0, source.sxy, dt, N, dx, s_source_pos_index)
+ if flags.source_sxz:
+ source.sxz = scale_stress_source(source, c0, flags.source_sxz, flags.source_p0, source.sxz, dt, N, dx, s_source_pos_index)
+ if flags.source_syz:
+ source.syz = scale_stress_source(source, c0, flags.source_syz, flags.source_p0, source.syz, dt, N, dx, s_source_pos_index)
+
+
+def scale_stress_source(source, c0, is_source_exists, is_p0_exists, source_s, dt, N, dx, s_source_pos_index):
if is_source_exists:
if source.s_mode == "dirichlet" or is_p0_exists:
- source_val = source_val / N
+ source_s = source_s / N
+ print(f"source.s_mode == dirichlet or is_p0_exists divide {source_s} by {N}")
else:
if c0.size == 1:
+ print("if c0.size == 1")
# compute the scale parameter based on the homogeneous sound
# speed
- source_val = source_val * (2 * dt * c0 / (N * dx))
+ source_s = source_s * (2 * dt * c0 / (N * dx))
else:
# compute the scale parameter seperately for each source
# position based on the sound speed at that position
- s_index = range(source_val.size[0])
- source_val[s_index, :] = source_val[s_index, :] * (2 * dt * c0[s_source_pos_index[s_index]] / (N * dx))
- return source_val
+ s_index = range(0, np.shape(source_s)[0])
+ print("s_index:", s_index)
+ mask = s_source_pos_index.flatten("F")[s_index]
+ print("mask:", mask)
+ scale = (2.0 * dt * np.expand_dims(c0.ravel(order="F")[mask.ravel(order="F")], axis=-1)) / (N * dx)
+ print("scale:", scale, "from:", dt, N, dx, np.expand_dims(c0.ravel(order="F")[mask.ravel(order="F")], axis=-1))
+ print(np.shape(source_s[s_index, :]), np.shape(scale))
+ source_s[s_index, :] = source_s[s_index, :] * scale
+
+ return source_s
def apply_velocity_source_corrections(
@@ -265,33 +289,35 @@ def apply_velocity_source_corrections(
def apply_source_correction(source_val, frequency_ref, dt):
- return source_val * math.cos(2 * math.pi * frequency_ref * dt / 2)
+ return source_val * np.cos(2.0 * np.pi * frequency_ref * dt)
def scale_velocity_sources(flags, source, kgrid, c0, dt, dx, dy, dz, u_source_pos_index):
- source.ux = scale_velocity_source_x(
- flags.source_ux, source.u_mode, source.ux, kgrid, c0, dt, dx, u_source_pos_index, flags.nonuniform_grid
- )
- source.uy = scale_velocity_source(flags.source_uy, source.u_mode, source.uy, c0, dt, u_source_pos_index, dy)
- source.uz = scale_velocity_source(flags.source_uz, source.u_mode, source.uz, c0, dt, u_source_pos_index, dz)
+ # source.ux = scale_velocity_source_x(
+ # flags.source_ux, source.u_mode, source.ux, kgrid, c0, dt, dx, u_source_pos_index, flags.nonuniform_grid
+ # )
+ source.ux = scale_velocity_source(flags.source_ux, source.u_mode, deepcopy(source.ux), c0, dt, u_source_pos_index, dx)
+ source.uy = scale_velocity_source(flags.source_uy, source.u_mode, deepcopy(source.uy), c0, dt, u_source_pos_index, dy)
+ source.uz = scale_velocity_source(flags.source_uz, source.u_mode, deepcopy(source.uz), c0, dt, u_source_pos_index, dz)
-def scale_velocity_source_x(is_source_ux, source_u_mode, source_val, kgrid, c0, dt, dx, u_source_pos_index, is_nonuniform_grid):
- """
- if source.u_mode is not set to 'dirichlet', scale the x-direction
- velocity source terms by 2*dt*c0/dx to account for the time step and
- convert to units of [m/s^2]
- Returns:
- """
- if not is_source_ux or source_u_mode == "dirichlet":
- return
+# def scale_velocity_source_x(is_source_ux, source_u_mode, source_val, kgrid, c0, dt, dx, u_source_pos_index, is_nonuniform_grid):
+# """
+# if source.u_mode is not set to 'dirichlet', scale the x-direction
+# velocity source terms by 2*dt*c0/dx to account for the time step and
+# convert to units of [m/s^2]
+# Returns:
- if is_nonuniform_grid:
- source_val = scale_velocity_source_nonuniform(is_source_ux, source_u_mode, kgrid, source_val, c0, dt, u_source_pos_index)
- else:
- source_val = scale_velocity_source(is_source_ux, source_u_mode, source_val, c0, dt, u_source_pos_index, dx)
- return source_val
+# """
+# if not is_source_ux or source_u_mode == "dirichlet":
+# return
+
+# if is_nonuniform_grid:
+# source_val = scale_velocity_source_nonuniform(is_source_ux, source_u_mode, kgrid, source_val, c0, dt, u_source_pos_index)
+# else:
+# source_val = scale_velocity_source(is_source_ux, source_u_mode, source_val, c0, dt, u_source_pos_index, dx)
+# return source_val
def scale_velocity_source(is_source, source_u_mode, source_val, c0, dt, u_source_pos_index, d_direction):
@@ -308,7 +334,7 @@ def scale_velocity_source(is_source, source_u_mode, source_val, c0, dt, u_source
u_source_pos_index:
d_direction:
- Returns:
+ Returns: scaled source_val
"""
if not is_source or source_u_mode == "dirichlet":
@@ -318,10 +344,14 @@ def scale_velocity_source(is_source, source_u_mode, source_val, c0, dt, u_source
# compute the scale parameter based on the homogeneous sound speed
source_val = source_val * (2 * c0 * dt / d_direction)
else:
- # compute the scale parameter seperately for each source position
- # based on the sound speed at that position
- u_index = range(source_val.size[0])
- source_val[u_index, :] = source_val[u_index, :] * (2 * c0[u_source_pos_index[u_index]] * dt / d_direction)
+
+ # compute the scale parameter seperately for each source position based on the
+ # sound speed at that position
+ u_index = range(source_val[:, 0].size)
+ mask = u_source_pos_index.flatten("F")[u_index]
+ scale = 2.0 * np.expand_dims(c0.ravel(order="F")[mask.ravel(order="F")], axis=-1) * dt / d_direction
+ source_val[u_index, :] *= scale
+
return source_val
diff --git a/kwave/kgrid.py b/kwave/kgrid.py
index ef5929c85..dcf240a74 100644
--- a/kwave/kgrid.py
+++ b/kwave/kgrid.py
@@ -108,11 +108,13 @@ def t_array(self):
@t_array.setter
def t_array(self, t_array):
# check for 'auto' input
- if t_array == "auto":
- # set values to auto
- self.Nt = "auto"
- self.dt = "auto"
-
+ if isinstance(t_array, str):
+ if t_array.lower() == "auto":
+ # set values to auto
+ self.Nt = "auto"
+ self.dt = "auto"
+ else:
+ raise ValueError("Wrong entry for t_array")
else:
# extract property values
Nt_temp = t_array.size
diff --git a/kwave/kmedium.py b/kwave/kmedium.py
index 962bcc1e4..c7c3635c8 100644
--- a/kwave/kmedium.py
+++ b/kwave/kmedium.py
@@ -11,28 +11,56 @@
class kWaveMedium(object):
# sound speed distribution within the acoustic medium [m/s] | required to be defined
sound_speed: np.array
+
# reference sound speed used within the k-space operator (phase correction term) [m/s]
sound_speed_ref: np.array = None
+
# density distribution within the acoustic medium [kg/m^3]
density: np.array = None
+
# power law absorption coefficient [dB/(MHz^y cm)]
alpha_coeff: np.array = None
+
# power law absorption exponent
alpha_power: np.array = None
+
# optional input to force either the absorption or dispersion terms in the equation of state to be excluded;
# valid inputs are 'no_absorption' or 'no_dispersion'
alpha_mode: np.array = None
+
# frequency domain filter applied to the absorption and dispersion terms in the equation of state
alpha_filter: np.array = None
+
# two element array used to control the sign of absorption and dispersion terms in the equation of state
alpha_sign: np.array = None
+
# parameter of nonlinearity
BonA: np.array = None
+
# is the medium absorbing?
absorbing: bool = False
+
# is the medium absorbing stokes?
stokes: bool = False
+ # compressional sound speed [m/s]
+ sound_speed_compression: np.array = None
+
+ # reference compressional sound speed [m/s]
+ sound_speed_ref_compression: np.array = None
+
+ # shear wave speed [m/s]
+ sound_speed_shear: np.array = None
+
+ # reference shear wave speed [m/s]
+ sound_speed_ref_shear: np.array = None
+
+ # power law absorption for compressional waves coefficient [dB/(MHz^y cm)]
+ alpha_coeff_compression: np.array = None
+
+ # power law absorption for shearwaves coefficient [dB/(MHz^y cm)]
+ alpha_coeff_shear: np.array = None
+
# """
# Note: For heterogeneous medium parameters, medium.sound_speed and
# medium.density must be given in matrix form with the same dimensions as
@@ -180,51 +208,4 @@ def _check_absorbing_with_stokes(self):
# don't allow alpha_filter with stokes absorption (no variables are applied in k-space)
assert self.alpha_filter is None, (
"Input option medium.alpha_filter is not supported with the axisymmetric code " "or medium.alpha_mode = 'stokes'. "
- )
-
- ##########################################
- # Elastic-code related properties - raise error when accessed
- ##########################################
- _ELASTIC_CODE_ACCESS_ERROR_TEXT_ = "Elastic simulation and related properties are not supported!"
-
- @property
- def sound_speed_shear(self): # pragma: no cover
- """
- Shear sound speed (used in elastic simulations | not supported currently!)
- """
- raise NotImplementedError(self._ELASTIC_CODE_ACCESS_ERROR_TEXT_)
-
- @property
- def sound_speed_ref_shear(self): # pragma: no cover
- """
- Shear sound speed reference (used in elastic simulations | not supported currently!)
- """
- raise NotImplementedError(self._ELASTIC_CODE_ACCESS_ERROR_TEXT_)
-
- @property
- def sound_speed_compression(self): # pragma: no cover
- """
- Compression sound speed (used in elastic simulations | not supported currently!)
- """
- raise NotImplementedError(self._ELASTIC_CODE_ACCESS_ERROR_TEXT_)
-
- @property
- def sound_speed_ref_compression(self): # pragma: no cover
- """
- Compression sound speed reference (used in elastic simulations | not supported currently!)
- """
- raise NotImplementedError(self._ELASTIC_CODE_ACCESS_ERROR_TEXT_)
-
- @property
- def alpha_coeff_compression(self): # pragma: no cover
- """
- Compression alpha coefficient (used in elastic simulations | not supported currently!)
- """
- raise NotImplementedError(self._ELASTIC_CODE_ACCESS_ERROR_TEXT_)
-
- @property
- def alpha_coeff_shear(self): # pragma: no cover
- """
- Shear alpha coefficient (used in elastic simulations | not supported currently!)
- """
- raise NotImplementedError(self._ELASTIC_CODE_ACCESS_ERROR_TEXT_)
+ )
\ No newline at end of file
diff --git a/kwave/ksensor.py b/kwave/ksensor.py
index 2b49b134d..b70979465 100644
--- a/kwave/ksensor.py
+++ b/kwave/ksensor.py
@@ -10,6 +10,7 @@ def __init__(self, mask=None, record=None):
self._mask = mask
# cell array of the acoustic parameters to record in the form Recorder
self.record = record
+
# record the time series from the beginning by default
# time index at which the sensor should start recording the data specified by sensor.record
self._record_start_index = 1
@@ -19,6 +20,7 @@ def __init__(self, mask=None, record=None):
# time varying pressure enforced as a Dirichlet boundary condition over sensor.mask
self.time_reversal_boundary_data = None
+
# two element array specifying the center frequency and percentage bandwidth
# of a frequency domain Gaussian filter applied to the sensor_data
self.frequency_response = None
@@ -45,7 +47,8 @@ def expand_grid(self, expand_size) -> None:
Returns:
None
"""
- self.mask = expand_matrix(self.mask, expand_size, 0)
+ if self.mask is not None:
+ self.mask = expand_matrix(self.mask, expand_size, 0)
@property
def record_start_index(self):
diff --git a/kwave/ksource.py b/kwave/ksource.py
index bed5a1d61..e4f3d7ead 100644
--- a/kwave/ksource.py
+++ b/kwave/ksource.py
@@ -4,6 +4,7 @@
import numpy as np
from kwave.kgrid import kWaveGrid
+#from kwave.utils.checks import enforce_fields
from kwave.utils.matrix import num_dim2
@@ -43,12 +44,14 @@ class kSource(object):
s_mask = None #: Stress source mask
s_mode = None #: Stress source mode
+
def is_p0_empty(self) -> bool:
"""
Check if the `p0` field is set and not empty
"""
return self.p0 is None or len(self.p0) == 0 or (np.sum(self.p0 != 0) == 0)
+
@property
def p0(self):
"""
@@ -56,15 +59,19 @@ def p0(self):
"""
return self._p0
+
@p0.setter
def p0(self, val):
- # check size and contents
+ """
+ check size and contents
+ """
if len(val) == 0 or np.sum(val != 0) == 0:
# if the initial pressure is empty, remove field
self._p0 = None
else:
self._p0 = val
+
def validate(self, kgrid: kWaveGrid) -> None:
"""
Validate the object fields for correctness
@@ -76,9 +83,21 @@ def validate(self, kgrid: kWaveGrid) -> None:
None
"""
if self.p0 is not None:
- if self.p0.shape != kgrid.k.shape:
- # throw an error if p0 is not the correct size
- raise ValueError("source.p0 must be the same size as the computational grid.")
+ #try:
+
+ if kgrid.k.shape[1] == 1:
+ kgrid.k = kgrid.k.flatten()
+
+ if (np.squeeze(self.p0.shape) != np.squeeze(kgrid.k.shape)).any():
+ msg = f"source.p0 must be the same size as the computational grid: {np.squeeze(self.p0.shape)} and {np.squeeze(kgrid.k.shape)}."
+ print(msg)
+ print(type(self.p0), type(kgrid.k), np.squeeze(self.p0.shape), np.squeeze(kgrid.k.shape), kgrid.k.ndim)
+ print(kgrid.k)
+ print(np.squeeze(kgrid.k))
+ # throw an error if p0 is not the correct size
+ raise ValueError(msg)
+ #except ValueError:
+ # print(f"source.p0 must be the same size as the computational grid: {self.p0.shape} and {kgrid.k.shape}.")
# if using the elastic code, reformulate source.p0 in terms of the
# stress source terms using the fact that source.p = [0.5 0.5] /
@@ -152,8 +171,9 @@ def validate(self, kgrid: kWaveGrid) -> None:
# check for time varying velocity source input and set source flag
if any([(getattr(self, k) is not None) for k in ["ux", "uy", "uz", "u_mask"]]):
+
# force u_mask to be given
- assert self.u_mask is not None
+ assert self.u_mask is not None, "source.u_mask must be defined"
# check mask is the correct size
assert (
@@ -175,7 +195,7 @@ def validate(self, kgrid: kWaveGrid) -> None:
if self.u_frequency_ref is not None:
# check frequency is a scalar, positive number
u_frequency_ref = self.u_frequency_ref
- assert np.isscalar(u_frequency_ref) and u_frequency_ref > 0
+ assert np.isscalar(u_frequency_ref) and u_frequency_ref > 0, "source.u_frequency_ref must be a scalar greater than zero"
# check frequency is within range
assert self.u_frequency_ref <= (
@@ -202,7 +222,7 @@ def validate(self, kgrid: kWaveGrid) -> None:
if u_unique.size <= 2 and u_unique.sum() == 1:
# if more than one time series is given, check the number of time
# series given matches the number of source elements
- ux_size = self.ux[:, 0].size
+ ux_size = self.ux[:, 0].size if (self.ux is not None) else None
uy_size = self.uy[:, 0].size if (self.uy is not None) else None
uz_size = self.uz[:, 0].size if (self.uz is not None) else None
u_sum = np.sum(self.u_mask)
@@ -225,128 +245,107 @@ def validate(self, kgrid: kWaveGrid) -> None:
or (self.flag_uz and (uz_size != u_sum))
):
raise ValueError(
- "The number of time series in source.ux (etc) " "must match the number of source elements in source.u_mask."
+ "The number of time series in source.ux (etc) " "must match the number of source elements in source.u_mask." +
+ str(ux_size) + ", " + str(u_sum)
)
else:
- raise NotImplementedError
+ #raise NotImplementedError
# check the source labels are monotonic, and start from 1
# if (sum(u_unique(2:end) - u_unique(1:end-1)) != (numel(u_unique) - 1)) or (~any(u_unique == 1))
- if eng.eval("(sum(u_unique(2:end) - " "u_unique(1:end-1)) ~= " "(numel(u_unique) - 1)) " "|| " "(~any(u_unique == 1))"):
+ if np.sum(u_unique[1:] - u_unique[:-2]) != np.size(u_unique) or not np.any(u_unique == 1):
raise ValueError(
"If using a labelled source.u_mask, " "the source labels must be monotonically increasing and start from 1."
)
# if more than one time series is given, check the number of time
# series given matches the number of source elements
- # if (flgs.source_ux and (size(source.ux, 1) != (numel(u_unique) - 1))) or
- # (flgs.source_uy and (size(source.uy, 1) != (numel(u_unique) - 1))) or
- # (flgs.source_uz and (size(source.uz, 1) != (numel(u_unique) - 1)))
- if eng.eval(
- "(flgs.source_ux && (size(source.ux, 1) ~= (numel(u_unique) - 1))) "
- "|| (flgs.source_uy && (size(source.uy, 1) ~= (numel(u_unique) - 1))) "
- "|| "
- "(flgs.source_uz && (size(source.uz, 1) ~= (numel(u_unique) - 1)))"
- ):
+ nonzero_labels: int = np.size(np.nonzero(u_unique))
+ # print(u_unique, np.size(np.nonzero(u_unique)), np.size(self.ux), np.shape(self.ux), np.size(u_unique) )
+ if (self.flag_ux > 0 and np.shape(self.ux)[0] != nonzero_labels or \
+ self.flag_uy > 0 and np.shape(self.uy)[0] != nonzero_labels or \
+ self.flag_uz > 0 and np.shape(self.uz)[0] != nonzero_labels):
raise ValueError(
"The number of time series in source.ux (etc) "
- "must match the number of labelled source elements in source.u_mask."
+ "must match the number of labelled source elements in source.u_mask.", np.size(self.ux)[0], np.size(u_unique)
)
# check for time varying stress source input and set source flag
if any([(getattr(self, k) is not None) for k in ["sxx", "syy", "szz", "sxy", "sxz", "syz", "s_mask"]]):
+
# force s_mask to be given
- enforce_fields(self, "s_mask")
+ assert hasattr(self, "s_mask")
# check mask is the correct size
- # if (numDim(source.s_mask) != kgrid.dim) or (all(size(source.s_mask) != size(kgrid.k)))
- if eng.eval("(numDim(source.s_mask) ~= kgrid.dim) || (all(size(source.s_mask) ~= size(kgrid.k)))"):
+ if (self.s_mask.ndim != kgrid.dim) or (np.shape(self.s_mask) != np.shape(kgrid.k)):
raise ValueError("source.s_mask must be the same size as the computational grid.")
# check mask is not empty
- assert np.array(eng.getfield(source, "s_mask")) != 0, "source.s_mask must be a binary grid with at least one element set to 1."
+ assert np.asarray(self.s_mask).sum() != 0, "s_mask must be a binary grid with at least one element set to 1."
# check the source mode input is valid
- if eng.isfield(source, "s_mode"):
- assert eng.getfield(source, "s_mode") in [
- "additive",
- "dirichlet",
- ], "source.s_mode must be set to ''additive'' or ''dirichlet''."
+ if hasattr(self, 's_mode') and (self.s_mode is not None):
+ assert self.s_mode in ["additive", "dirichlet"], "source.s_mode must be set to ''additive'' or ''dirichlet''."
else:
- eng.setfield(source, "s_mode", self.SOURCE_S_MODE_DEF)
+ self.s_mode = 'additive'
# set source flgs to the length of the sources, this allows the
# inputs to be defined independently and be of any length
- if self.sxx is not None and self_sxx > k_Nt:
- logging.log(logging.WARN, " source.sxx has more time points than kgrid.Nt," " remaining time points will not be used.")
- if self.syy is not None and self_syy > k_Nt:
- logging.log(logging.WARN, " source.syy has more time points than kgrid.Nt," " remaining time points will not be used.")
- if self.szz is not None and self_szz > k_Nt:
- logging.log(logging.WARN, " source.szz has more time points than kgrid.Nt," " remaining time points will not be used.")
- if self.sxy is not None and self_sxy > k_Nt:
- logging.log(logging.WARN, " source.sxy has more time points than kgrid.Nt," " remaining time points will not be used.")
- if self.sxz is not None and self_sxz > k_Nt:
- logging.log(logging.WARN, " source.sxz has more time points than kgrid.Nt," " remaining time points will not be used.")
- if self.syz is not None and self_syz > k_Nt:
- logging.log(logging.WARN, " source.syz has more time points than kgrid.Nt," " remaining time points will not be used.")
-
- # create an indexing variable corresponding to the location of all
- # the source elements
- raise NotImplementedError
-
- # check if the mask is binary or labelled
- "s_unique = unique(source.s_mask);"
+ if self.sxx is not None and np.max(np.shape(self.sxx)) > kgrid.Nt:
+ logging.log(logging.WARN, " source.sxx has more time points than kgrid.Nt, remaining time points will not be used - " + str(np.max(np.shape(self.sxx))))
+ if self.syy is not None and np.max(np.shape(self.syy)) > kgrid.Nt:
+ logging.log(logging.WARN, " source.syy has more time points than kgrid.Nt, remaining time points will not be used - " + str(np.max(np.shape(self.syy))))
+ if self.szz is not None and np.max(np.shape(self.szz)) > kgrid.Nt:
+ logging.log(logging.WARN, " source.szz has more time points than kgrid.Nt, remaining time points will not be used - " + str(np.max(np.shape(self.szz))))
+ if self.sxy is not None and np.max(np.shape(self.sxy)) > kgrid.Nt:
+ logging.log(logging.WARN, " source.sxy has more time points than kgrid.Nt, remaining time points will not be used - " + str(np.max(np.shape(self.sxy))))
+ if self.sxz is not None and np.max(np.shape(self.sxz)) > kgrid.Nt:
+ logging.log(logging.WARN, " source.sxz has more time points than kgrid.Nt, remaining time points will not be used - " + str(np.max(np.shape(self.sxz))))
+ if self.syz is not None and np.max(np.shape(self.syz)) > kgrid.Nt:
+ logging.log(logging.WARN, " source.syz has more time points than kgrid.Nt, remaining time points will not be used - " + str(np.max(np.shape(self.syz))))
+
+ # create an indexing variable corresponding to the location of all the source elements
+ # raise NotImplementedError
+
+ # check if the mask is binary or labelled: if binary then only (0,1) so sum is 1
+ s_unique = np.unique(self.s_mask)
# create a second indexing variable
- if eng.eval("numel(s_unique) <= 2 && sum(s_unique) == 1"):
- s_mask = eng.getfield(source, "s_mask")
- s_mask_sum = np.array(s_mask).sum()
+ if np.size(s_unique) <= 2 and np.sum(s_unique) == 1:
- # if more than one time series is given, check the number of time
- # series given matches the number of source elements
+ s_mask_sum = np.array(self.s_mask).sum()
+
+ # if more than one time series is given, check the number of time series given matches the number of source elements
if (
- (self.source_sxx and (eng.eval("length(source.sxx(:,1)) > 1))")))
- or (self.source_syy and (eng.eval("length(source.syy(:,1)) > 1))")))
- or (self.source_szz and (eng.eval("length(source.szz(:,1)) > 1))")))
- or (self.source_sxy and (eng.eval("length(source.sxy(:,1)) > 1))")))
- or (self.source_sxz and (eng.eval("length(source.sxz(:,1)) > 1))")))
- or (self.source_syz and (eng.eval("length(source.syz(:,1)) > 1))")))
- ):
- if (
- (self.source_sxx and (eng.eval("length(source.sxx(:,1))") != s_mask_sum))
- or (self.source_syy and (eng.eval("length(source.syy(:,1))") != s_mask_sum))
- or (self.source_szz and (eng.eval("length(source.szz(:,1))") != s_mask_sum))
- or (self.source_sxy and (eng.eval("length(source.sxy(:,1))") != s_mask_sum))
- or (self.source_sxz and (eng.eval("length(source.sxz(:,1))") != s_mask_sum))
- or (self.source_syz and (eng.eval("length(source.syz(:,1))") != s_mask_sum))
- ):
- raise ValueError(
- "The number of time series in source.sxx (etc) " "must match the number of source elements in source.s_mask."
- )
+ (self.sxx is not None and np.size(self.sxx[:, 0]) > 1) or
+ (self.syy is not None and np.size(self.syy[:, 0]) > 1) or
+ (self.szz is not None and np.size(self.szz[:, 0]) > 1) or
+ (self.sxy is not None and np.size(self.sxy[:, 0]) > 1) or
+ (self.sxz is not None and np.size(self.sxz[:, 0]) > 1) or
+ (self.syz is not None and np.size(self.syz[:, 0]) > 1)) and \
+ ((self.sxx is not None and np.size(self.sxx[:, 0]) != s_mask_sum) or
+ (self.syy is not None and np.size(self.syy[:, 0]) != s_mask_sum) or
+ (self.szz is not None and np.size(self.szz[:, 0]) != s_mask_sum) or
+ (self.sxy is not None and np.size(self.sxy[:, 0]) != s_mask_sum) or
+ (self.sxz is not None and np.size(self.sxz[:, 0]) != s_mask_sum) or
+ (self.syz is not None and np.size(self.syz[:, 0]) != s_mask_sum)):
+ raise ValueError("The number of time series in source.sxx (etc) must match the number of source elements in source.s_mask.")
else:
- # check the source labels are monotonic, and start from 1
- # if (sum(s_unique(2:end) - s_unique(1:end-1)) != (numel(s_unique) - 1)) or (~any(s_unique == 1))
- if eng.eval("(sum(s_unique(2:end) - s_unique(1:end-1)) ~= " "(numel(s_unique) - 1)) || (~any(s_unique == 1))"):
- raise ValueError(
- "If using a labelled source.s_mask, " "the source labels must be monotonically increasing and start from 1."
- )
+ # check the source labels are monotonic, and start from 0
+ if np.sum(s_unique[1:-1] - s_unique[0:-2]) != (np.size(s_unique) - 2) or (not (s_unique == 0).any()):
+ raise ValueError("If using a labelled source.s_mask, the source labels must be monotonically increasing and start from 0.")
- numel_s_unique = eng.eval("numel(s_unique) - 1;")
- # if more than one time series is given, check the number of time
- # series given matches the number of source elements
- if (
- (self.source_sxx and (eng.eval("size(source.sxx, 1)") != numel_s_unique))
- or (self.source_syy and (eng.eval("size(source.syy, 1)") != numel_s_unique))
- or (self.source_szz and (eng.eval("size(source.szz, 1)") != numel_s_unique))
- or (self.source_sxy and (eng.eval("size(source.sxy, 1)") != numel_s_unique))
- or (self.source_sxz and (eng.eval("size(source.sxz, 1)") != numel_s_unique))
- or (self.source_syz and (eng.eval("size(source.syz, 1)") != numel_s_unique))
- ):
- raise ValueError(
- "The number of time series in source.sxx (etc) "
- "must match the number of labelled source elements in source.u_mask."
- )
+ numel_s_unique: int = np.size(s_unique) - 1
+
+ # if more than one time series is given, check the number of time series given matches the number of source elements
+ if ((self.sxx is not None and np.shape(self.sxx)[0] != numel_s_unique) or
+ (self.syy is not None and np.shape(self.syy)[0] != numel_s_unique) or
+ (self.szz is not None and np.shape(self.szz)[0] != numel_s_unique) or
+ (self.sxy is not None and np.shape(self.sxy)[0] != numel_s_unique) or
+ (self.sxz is not None and np.shape(self.sxz)[0] != numel_s_unique) or
+ (self.syz is not None and np.shape(self.syz)[0] != numel_s_unique)):
+ raise ValueError("The number of time series in source.sxx (etc) must match the number of labelled source elements in source.u_mask.")
@property
def flag_ux(self):
@@ -362,7 +361,7 @@ def flag_ux(self):
@property
def flag_uy(self):
"""
- Get the length of the sources in X-direction, this allows the
+ Get the length of the sources in Y-direction, this allows the
inputs to be defined independently and be of any length
Returns:
@@ -373,7 +372,7 @@ def flag_uy(self):
@property
def flag_uz(self):
"""
- Get the length of the sources in X-direction, this allows the
+ Get the length of the sources in Z-direction, this allows the
inputs to be defined independently and be of any length
Returns:
diff --git a/kwave/kspaceFirstOrder1D.py b/kwave/kspaceFirstOrder1D.py
new file mode 100644
index 000000000..a258d1e8e
--- /dev/null
+++ b/kwave/kspaceFirstOrder1D.py
@@ -0,0 +1,975 @@
+import numpy as np
+import scipy.fft
+from tqdm import tqdm
+from typing import Union
+
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksensor import kSensor
+from kwave.ksource import kSource
+from kwave.kWaveSimulation import kWaveSimulation
+
+from kwave.ktransducer import NotATransducer
+
+from kwave.utils.data import scale_time
+from kwave.utils.math import sinc
+from kwave.utils.pml import get_pml
+from kwave.utils.tictoc import TicToc
+from kwave.utils.dotdictionary import dotdict
+
+from kwave.options.simulation_options import SimulationOptions
+
+from kwave.kWaveSimulation_helper import extract_sensor_data
+
+def kspace_first_order_1D(kgrid: kWaveGrid,
+ source: kSource,
+ sensor: Union[NotATransducer, kSensor],
+ medium: kWaveMedium,
+ simulation_options: SimulationOptions,
+ verbose: bool = False):
+
+ """
+ KSPACEFIRSTORDER1D 1D time-domain simulation of wave propagation.
+
+ DESCRIPTION:
+ kspaceFirstOrder1D simulates the time-domain propagation of
+ compressional waves through a one-dimensional homogeneous or
+ heterogeneous acoustic medium given four input structures: kgrid,
+ medium, source, and sensor. The computation is based on a first-order
+ k-space model which accounts for power law absorption and a
+ heterogeneous sound speed and density. If medium.BonA is specified,
+ cumulative nonlinear effects are also modelled. At each time-step
+ (defined by kgrid.dt and kgrid.Nt or kgrid.t_array), the acoustic
+ field parameters at the positions defined by sensor.mask are recorded
+ and stored. If kgrid.t_array is set to 'auto', this array is
+ automatically generated using the makeTime method of the kWaveGrid
+ class. An anisotropic absorbing boundary layer called a perfectly
+ matched layer (PML) is implemented to prevent waves that leave one
+ side of the domain being reintroduced from the opposite side (a
+ consequence of using the FFT to compute the spatial derivatives in
+ the wave equation). This allows infinite domain simulations to be
+ computed using small computational grids.
+
+ For a homogeneous medium the formulation is exact and the time-steps
+ are only limited by the effectiveness of the perfectly matched layer.
+ For a heterogeneous medium, the solution represents a leap-frog
+ pseudospectral method with a k-space correction that improves the
+ accuracy of computing the temporal derivatives. This allows larger
+ time-steps to be taken for the same level of accuracy compared to
+ conventional pseudospectral time-domain methods. The computational
+ grids are staggered both spatially and temporally.
+
+ An initial pressure distribution can be specified by assigning a
+ matrix (the same size as the computational grid) of arbitrary numeric
+ values to source.p0. A time varying pressure source can similarly be
+ specified by assigning a binary matrix (i.e., a matrix of 1's and 0's
+ with the same dimensions as the computational grid) to source.p_mask
+ where the 1's represent the grid points that form part of the source.
+ The time varying input signals are then assigned to source.p. This
+ can be a single time series (in which case it is applied to all
+ source elements), or a matrix of time series following the source
+ elements using MATLAB's standard column-wise linear matrix index
+ ordering. A time varying velocity source can be specified in an
+ analogous fashion, where the source location is specified by
+ source.u_mask, and the time varying input velocity is assigned to
+ source.ux.
+
+ The field values are returned as arrays of time series at the sensor
+ locations defined by sensor.mask. This can be defined in three
+ different ways. (1) As a binary matrix (i.e., a matrix of 1's and 0's
+ with the same dimensions as the computational grid) representing the
+ grid points within the computational grid that will collect the data.
+ (2) As the grid coordinates of two opposing ends of a line in the
+ form [x1; x2]. This is equivalent to using a binary sensor mask
+ covering the same region, however, the output is indexed differently
+ as discussed below. (3) As a series of Cartesian coordinates within
+ the grid which specify the location of the pressure values stored at
+ each time step. If the Cartesian coordinates don't exactly match the
+ coordinates of a grid point, the output values are calculated via
+ interpolation. The Cartesian points must be given as a 1 by N matrix
+ corresponding to the x positions, where the Cartesian origin is
+ assumed to be in the center of the grid. If no output is required,
+ the sensor input can be replaced with an empty array [].
+
+ If sensor.mask is given as a set of Cartesian coordinates, the
+ computed sensor_data is returned in the same order. If sensor.mask is
+ given as a binary matrix, sensor_data is returned using MATLAB's
+ standard column-wise linear matrix index ordering. In both cases, the
+ recorded data is indexed as sensor_data(sensor_point_index,
+ time_index). For a binary sensor mask, the field values at a
+ particular time can be restored to the sensor positions within the
+ computation grid using unmaskSensorData. If sensor.mask is given as a
+ list of opposing ends of a line, the recorded data is indexed as
+ sensor_data(line_index).p(x_index, time_index), where x_index
+ corresponds to the grid index within the line, and line_index
+ corresponds to the number of lines if more than one is specified.
+
+ By default, the recorded acoustic pressure field is passed directly
+ to the output sensor_data. However, other acoustic parameters can
+ also be recorded by setting sensor.record to a cell array of the form
+ {'p', 'u', 'p_max', ...}. For example, both the particle velocity and
+ the acoustic pressure can be returned by setting sensor.record =
+ {'p', 'u'}. If sensor.record is given, the output sensor_data is
+ returned as a structure with the different outputs appended as
+ structure fields. For example, if sensor.record = {'p', 'p_final',
+ 'p_max', 'u'}, the output would contain fields sensor_data.p,
+ sensor_data.p_final, sensor_data.p_max, and sensor_data.ux. Most of
+ the output parameters are recorded at the given sensor positions and
+ are indexed as sensor_data.field(sensor_point_index, time_index) or
+ sensor_data(line_index).field(x_index, time_index) if using a sensor
+ mask defined as opposing ends of a line. The exceptions are the
+ averaged quantities ('p_max', 'p_rms', 'u_max', 'p_rms', 'I_avg'),
+ the 'all' quantities ('p_max_all', 'p_min_all', 'u_max_all',
+ 'u_min_all'), and the final quantities ('p_final', 'u_final'). The
+ averaged quantities are indexed as
+ sensor_data.p_max(sensor_point_index) or
+ sensor_data(line_index).p_max(x_index) if using line ends, while the
+ final and 'all' quantities are returned over the entire grid and are
+ always indexed as sensor_data.p_final(nx), regardless of the type of
+ sensor mask.
+
+ kspaceFirstOrder1D may also be used for time reversal image
+ reconstruction by assigning the time varying pressure recorded over
+ an arbitrary sensor surface to the input field
+ sensor.time_reversal_boundary_data. This data is then enforced in
+ time reversed order as a time varying Dirichlet boundary condition
+ over the sensor surface given by sensor.mask. The boundary data must
+ be indexed as sensor.time_reversal_boundary_data(sensor_point_index,
+ time_index). If sensor.mask is given as a set of Cartesian
+ coordinates, the boundary data must be given in the same order. An
+ equivalent binary sensor mask (computed using nearest neighbour
+ interpolation) is then used to place the pressure values into the
+ computational grid at each time step. If sensor.mask is given as a
+ binary matrix of sensor points, the boundary data must be ordered
+ using MATLAB's standard column-wise linear matrix indexing. If no
+ additional inputs are required, the source input can be replaced with
+ an empty array [].
+
+ Acoustic attenuation compensation can also be included during time
+ reversal image reconstruction by assigning the absorption parameters
+ medium.alpha_coeff and medium.alpha_power and reversing the sign of
+ the absorption term by setting medium.alpha_sign = [-1, 1]. This
+ forces the propagating waves to grow according to the absorption
+ parameters instead of decay. The reconstruction should then be
+ regularised by assigning a filter to medium.alpha_filter (this can be
+ created using getAlphaFilter).
+
+ Note: To run a simple photoacoustic image reconstruction example
+ using time reversal (that commits the 'inverse crime' of using the
+ same numerical parameters and model for data simulation and image
+ reconstruction), the sensor_data returned from a k-Wave simulation
+ can be passed directly to sensor.time_reversal_boundary_data with the
+ input fields source.p0 and source.p removed or set to zero.
+
+ USAGE:
+ sensor_data = kspaceFirstOrder1D(kgrid, medium, source, sensor)
+ sensor_data = kspaceFirstOrder1D(kgrid, medium, source, sensor, ...)
+
+ INPUTS:
+ The minimum fields that must be assigned to run an initial value problem
+ (for example, a photoacoustic forward simulation) are marked with a *.
+
+ kgrid* - k-Wave grid object returned by kWaveGrid
+ containing Cartesian and k-space grid fields
+ kgrid.t_array* - evenly spaced array of time values [s] (set
+ to 'auto' by kWaveGrid)
+
+
+ medium.sound_speed* - sound speed distribution within the acoustic
+ medium [m/s]
+ medium.sound_speed_ref - reference sound speed used within the
+ k-space operator (phase correction term)
+ [m/s]
+ medium.density* - density distribution within the acoustic
+ medium [kg/m^3]
+ medium.BonA - parameter of nonlinearity
+ medium.alpha_power - power law absorption exponent
+ medium.alpha_coeff - power law absorption coefficient
+ [dB/(MHz^y cm)]
+ medium.alpha_mode - optional input to force either the
+ absorption or dispersion terms in the
+ equation of state to be excluded; valid
+ inputs are 'no_absorption' or
+ 'no_dispersion'
+ medium.alpha_filter - frequency domain filter applied to the
+ absorption and dispersion terms in the
+ equation of state
+ medium.alpha_sign - two element array used to control the sign
+ of absorption and dispersion terms in the
+ equation of state
+
+
+ source.p0* - initial pressure within the acoustic medium
+ source.p - time varying pressure at each of the source
+ positions given by source.p_mask
+ source.p_mask - binary matrix specifying the positions of
+ the time varying pressure source
+ distribution
+ source.p_mode - optional input to control whether the input
+ pressure is injected as a mass source or
+ enforced as a dirichlet boundary condition;
+ valid inputs are 'additive' (the default) or
+ 'dirichlet'
+ source.ux - time varying particle velocity in the
+ x-direction at each of the source positions
+ given by source.u_mask
+ source.u_mask - binary matrix specifying the positions of
+ the time varying particle velocity
+ distribution
+ source.u_mode - optional input to control whether the input
+ velocity is applied as a force source or
+ enforced as a dirichlet boundary condition;
+ valid inputs are 'additive' (the default) or
+ 'dirichlet'
+
+
+ sensor.mask* - binary matrix or a set of Cartesian points
+ where the pressure is recorded at each
+ time-step
+ sensor.record - cell array of the acoustic parameters to
+ record in the form sensor.record = {'p',
+ 'u', ...}; valid inputs are:
+ 'p' (acoustic pressure)
+ 'p_max' (maximum pressure)
+ 'p_min' (minimum pressure)
+ 'p_rms' (RMS pressure)
+ 'p_final' (final pressure field at all grid points)
+ 'p_max_all' (maximum pressure at all grid points)
+ 'p_min_all' (minimum pressure at all grid points)
+ 'u' (particle velocity)
+ 'u_max' (maximum particle velocity)
+ 'u_min' (minimum particle velocity)
+ 'u_rms' (RMS particle velocity)
+ 'u_final' (final particle velocity field at all grid points)
+ 'u_max_all' (maximum particle velocity at all grid points)
+ 'u_min_all' (minimum particle velocity at all grid points)
+ 'u_non_staggered' (particle velocity on non-staggered grid)
+ 'I' (time varying acoustic intensity)
+ 'I_avg' (average acoustic intensity)
+ sensor.record_start_index
+ - time index at which the sensor should start
+ recording the data specified by
+ sensor.record (default = 1)
+ sensor.time_reversal_boundary_data
+ - time varying pressure enforced as a
+ Dirichlet boundary condition over
+ sensor.mask
+ sensor.frequency_response
+ - two element array specifying the center
+ frequency and percentage bandwidth of a
+ frequency domain Gaussian filter applied to
+ the sensor_data
+
+ Note: For heterogeneous medium parameters, medium.sound_speed and
+ medium.density must be given in matrix form with the same dimensions as
+ kgrid. For homogeneous medium parameters, these can be given as single
+ numeric values. If the medium is homogeneous and velocity inputs or
+ outputs are not required, it is not necessary to specify medium.density.
+
+ OPTIONAL INPUTS:
+ Optional 'string', value pairs that may be used to modify the default
+ computational settings.
+
+ 'CartInterp' - Interpolation mode used to extract the
+ pressure when a Cartesian sensor mask is
+ given. If set to 'nearest' and more than one
+ Cartesian point maps to the same grid point,
+ duplicated data points are discarded and
+ sensor_data will be returned with less
+ points than that specified by sensor.mask
+ (default = 'linear').
+ 'CreateLog' - Boolean controlling whether the command line
+ output is saved using the diary function
+ with a date and time stamped filename
+ (default = False).
+ 'DataCast' - String input of the data type that variables
+ are cast to before computation. For example,
+ setting to 'single' will speed up the
+ computation time (due to the improved
+ efficiency of fftn and ifftn for this data
+ type) at the expense of a loss in precision.
+ This variable is also useful for utilising
+ GPU parallelisation through libraries such
+ as the Parallel Computing Toolbox by setting
+ 'DataCast' to 'gpuArray-single' (default =
+ 'off').
+ 'DataRecast' - Boolean controlling whether the output data
+ is cast back to double precision. If set to
+ False, sensor_data will be returned in the
+ data format set using the 'DataCast' option.
+ 'DisplayMask' - Binary matrix overlaid onto the animated
+ simulation display. Elements set to 1 within
+ the display mask are set to black within the
+ display (default = sensor.mask).
+ 'LogScale' - Boolean controlling whether the pressure
+ field is log compressed before display
+ (default = False). The data is compressed by
+ scaling both the positive and negative
+ values between 0 and 1 (truncating the data
+ to the given plot scale), adding a scalar
+ value (compression factor) and then using
+ the corresponding portion of a log10 plot
+ for the compression (the negative parts are
+ remapped to be negative thus the default
+ color scale will appear unchanged). The
+ amount of compression can be controlled by
+ adjusting the compression factor which can
+ be given in place of the Boolean input. The
+ closer the compression factor is to zero,
+ the steeper the corresponding part of the
+ log10 plot used, and the greater the
+ compression (the default compression factor
+ is 0.02).
+ 'MovieArgs' - Settings for VideoWriter. Parameters must be
+ given as {'param', value, ...} pairs within
+ a cell array (default = {}), where 'param'
+ corresponds to a writable property of a
+ VideoWriter object.
+ 'MovieName' - Name of the movie produced when
+ 'RecordMovie' is set to true (default =
+ 'date-time-kspaceFirstOrder1D').
+ 'MovieProfile' - Profile input passed to VideoWriter.
+ 'PlotFreq' - The number of iterations which must pass
+ before the simulation plot is updated
+ (default = 10).
+ 'PlotLayout' - Boolean controlling whether a four panel
+ plot of the initial simulation layout is
+ produced (initial pressure, sensor mask,
+ sound speed, density) (default = False).
+ 'PlotPML' - Boolean controlling whether the perfectly
+ matched layer is shown in the simulation
+ plots. If set to False, the PML is not
+ displayed (default = true).
+ 'PlotScale' - [min, max] values used to control the
+ scaling for imagesc (visualisation). If set
+ to 'auto', a symmetric plot scale is chosen
+ automatically for each plot frame.
+ 'PlotSim' - Boolean controlling whether the simulation
+ iterations are progressively plotted
+ (default = true).
+ 'PMLAlpha' - Absorption within the perfectly matched
+ layer in Nepers per grid point (default =
+ 2).
+ 'PMLInside' - Boolean controlling whether the perfectly
+ matched layer is inside or outside the grid.
+ If set to False, the input grids are
+ enlarged by PMLSize before running the
+ simulation (default = true).
+ 'PMLSize' - Size of the perfectly matched layer in grid
+ points. To remove the PML, set the
+ appropriate PMLAlpha to zero rather than
+ forcing the PML to be of zero size (default
+ = 20).
+ 'RecordMovie' - Boolean controlling whether the displayed
+ image frames are captured and stored as a
+ movie using VideoWriter (default = False).
+ 'Smooth' - Boolean controlling whether source.p0,
+ medium.sound_speed, and medium.density are
+ smoothed using smooth before computation.
+ 'Smooth' can either be given as a single
+ Boolean value or as a 3 element array to
+ control the smoothing of source.p0,
+ medium.sound_speed, and medium.density,
+ independently (default = [true, False,
+ False]).
+
+ OUTPUTS:
+ If sensor.record is not defined by the user:
+ sensor_data - time varying pressure recorded at the sensor
+ positions given by sensor.mask
+
+ If sensor.record is defined by the user:
+ sensor_data.p - time varying pressure recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'p' is set)
+ sensor_data.p_max - maximum pressure recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'p_max' is set)
+ sensor_data.p_min - minimum pressure recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'p_min' is set)
+ sensor_data.p_rms - rms of the time varying pressure recorded at
+ the sensor positions given by sensor.mask
+ (returned if 'p_rms' is set)
+ sensor_data.p_final - final pressure field at all grid points
+ within the domain (returned if 'p_final' is
+ set)
+ sensor_data.p_max_all - maximum pressure recorded at all grid points
+ within the domain (returned if 'p_max_all'
+ is set)
+ sensor_data.p_min_all - minimum pressure recorded at all grid points
+ within the domain (returned if 'p_min_all'
+ is set)
+ sensor_data.ux - time varying particle velocity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'u' is
+ set)
+ sensor_data.ux_max - maximum particle velocity in the x-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_max' is set)
+ sensor_data.ux_min - minimum particle velocity in the x-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_min' is set)
+ sensor_data.ux_rms - rms of the time varying particle velocity in
+ the x-direction recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_rms' is set)
+ sensor_data.ux_final - final particle velocity field in the
+ x-direction at all grid points within the
+ domain (returned if 'u_final' is set)
+ sensor_data.ux_max_all - maximum particle velocity in the x-direction
+ recorded at all grid points within the
+ domain (returned if 'u_max_all' is set)
+ sensor_data.ux_min_all - minimum particle velocity in the x-direction
+ recorded at all grid points within the
+ domain (returned if 'u_min_all' is set)
+ sensor_data.ux_non_staggered
+ - time varying particle velocity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask after shifting to the
+ non-staggered grid (returned if
+ 'u_non_staggered' is set)
+ sensor_data.Ix - time varying acoustic intensity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I' is
+ set)
+ sensor_data.Ix_avg - average acoustic intensity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I_avg' is
+ set)
+
+ ABOUT:
+ author - Bradley Treeby and Ben Cox
+ date - 22nd April 2009
+ last update - 25th July 2019
+
+ This function is part of the k-Wave Toolbox (http://www.k-wave.org)
+ Copyright (C) 2009-2019 Bradley Treeby and Ben Cox
+
+ See also kspaceFirstOrderAS, kspaceFirstOrder2D, kspaceFirstOrder3D,
+ kWaveGrid, kspaceSecondOrder
+
+ This file is part of k-Wave. k-Wave is free software: you can
+ redistribute it and/or modify it under the terms of the GNU Lesser
+ General Public License as published by the Free Software Foundation,
+ either version 3 of the License, or (at your option) any later version.
+
+ k-Wave is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with k-Wave. If not, see .
+
+ """
+
+ # =========================================================================
+ # CHECK INPUT STRUCTURES AND OPTIONAL INPUTS
+ # =========================================================================
+
+ # start the timer and store the start time
+ timer = TicToc()
+ timer.tic()
+
+ # run script to check inputs and create the required arrays
+ k_sim = kWaveSimulation(kgrid=kgrid, source=source, sensor=sensor, medium=medium,
+ simulation_options=simulation_options)
+
+ # this will create the sensor_data dotdict
+ k_sim.input_checking("kspaceFirstOrder1D")
+
+ # aliases from simulation
+ sensor_data = k_sim.sensor_data
+ options = k_sim.options
+ record = k_sim.record
+
+ # =========================================================================
+ # CALCULATE MEDIUM PROPERTIES ON STAGGERED GRID
+ # =========================================================================
+
+ # interpolate the values of the density at the staggered grid locations
+ # where sgx = (x + dx/2)
+ rho0 = k_sim.rho0
+ m_rho0: int = np.squeeze(rho0).ndim
+
+ if (m_rho0 > 0 and options.use_sg):
+
+ points = np.squeeze(k_sim.kgrid.x_vec)
+
+ # rho0 is heterogeneous and staggered grids are used
+ rho0_sgx = np.interp(points + k_sim.kgrid.dx / 2.0, points, np.squeeze(k_sim.rho0))
+
+ # set values outside of the interpolation range to original values
+ rho0_sgx[np.isnan(rho0_sgx)] = np.squeeze(k_sim.rho0)[np.isnan(rho0_sgx)]
+
+ else:
+ # rho0 is homogeneous or staggered grids are not used
+ rho0_sgx = k_sim.rho0
+
+ # invert rho0 so it doesn't have to be done each time step
+ rho0_sgx_inv = 1.0 / rho0_sgx
+
+ rho0_sgx_inv = rho0_sgx_inv[:, np.newaxis]
+
+ # clear unused variables
+ # del rho0_sgx
+
+ # =========================================================================
+ # PREPARE DERIVATIVE AND PML OPERATORS
+ # =========================================================================
+
+ # get the regular PML operators based on the reference sound speed and PML settings
+ Nx = k_sim.kgrid.Nx
+ dx = k_sim.kgrid.dx
+ dt = k_sim.kgrid.dt
+ Nt = k_sim.kgrid.Nt
+
+ pml_x_alpha = options.pml_x_alpha
+ pml_x_size = options.pml_x_size
+ c_ref = k_sim.c_ref
+
+ kx_vec = np.squeeze(k_sim.kgrid.k_vec[0])
+
+ c0 = medium.sound_speed
+
+ # get the PML operators based on the reference sound speed and PML settings
+ pml_x = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, False, 0).T
+ pml_x_sgx = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, True, 0).T
+
+ # define the k-space derivative operator
+ ddx_k = scipy.fft.ifftshift(1j * kx_vec)
+ ddx_k = ddx_k[:, np.newaxis]
+
+ # define the staggered grid shift operators (the option options.use_sg exists for debugging)
+ if options.use_sg:
+ ddx_k_shift_pos = scipy.fft.ifftshift( np.exp( 1j * kx_vec * dx / 2.0))
+ ddx_k_shift_neg = scipy.fft.ifftshift( np.exp(-1j * kx_vec * dx / 2.0))
+ ddx_k_shift_pos = ddx_k_shift_pos[:, np.newaxis]
+ ddx_k_shift_neg = ddx_k_shift_neg[:, np.newaxis]
+ else:
+ ddx_k_shift_pos = 1.0
+ ddx_k_shift_neg = 1.0
+
+
+ # create k-space operator (the option options.use_kspace exists for debugging)
+ if options.use_kspace:
+ kappa = scipy.fft.ifftshift(sinc(c_ref * kgrid.k * kgrid.dt / 2.0))
+ kappa = kappa[:, np.newaxis]
+ if (hasattr(options, 'source_p') and hasattr(k_sim.source, 'p_mode')) and (k_sim.source.p_mode == 'additive') or \
+ (hasattr(options, 'source_ux') and hasattr(k_sim.source, 'u_mode')) and (k_sim.source.u_mode == 'additive'):
+ source_kappa = scipy.fft.ifftshift(np.cos (c_ref * kgrid.k * kgrid.dt / 2.0))
+ source_kappa = source_kappa[:, np.newaxis]
+ else:
+ kappa = 1.0
+ source_kappa = 1.0
+
+
+ # =========================================================================
+ # DATA CASTING
+ # =========================================================================
+
+ # preallocate the loop variables using the castZeros anonymous function
+ # (this creates a matrix of zeros in the data type specified by data_cast)
+ if not (options.data_cast == 'off'):
+ myType = np.single
+ else:
+ myType = np.double
+
+ grid_shape = (Nx, 1)
+
+ # preallocate the loop variables
+ p = np.zeros(grid_shape, dtype=myType)
+ rhox = np.zeros(grid_shape, dtype=myType)
+ ux_sgx = np.zeros(grid_shape, dtype=myType)
+ p_k = np.zeros(grid_shape, dtype=myType)
+
+ c0 = c0.astype(myType)
+
+ verbose: bool = False
+
+ # =========================================================================
+ # CREATE INDEX VARIABLES
+ # =========================================================================
+
+ # setup the time index variable
+ if (not options.time_rev):
+ index_start: int = 0
+ index_step: int = 1
+ index_end: int = Nt
+ else:
+ # throw error for unsupported feature
+ raise TypeError('Time reversal using sensor.time_reversal_boundary_data is not currently supported.')
+
+ # reverse the order of the input data
+ sensor.time_reversal_boundary_data = np.fliplr(sensor.time_reversal_boundary_data)
+ index_start = 0
+ index_step = 0
+
+ # stop one time point before the end so the last points are not
+ # propagated
+ index_end = kgrid.Nt - 1
+
+ # These should be zero indexed
+ if hasattr(k_sim, 's_source_sig_index') and k_sim.s_source_pos_index is not None:
+ k_sim.s_source_pos_index = np.squeeze(k_sim.s_source_pos_index) - int(1)
+
+ if hasattr(k_sim, 'u_source_pos_index') and k_sim.u_source_pos_index is not None:
+ k_sim.u_source_pos_index = np.squeeze(k_sim.u_source_pos_index) - int(1)
+
+ if hasattr(k_sim, 'p_source_pos_index') and k_sim.p_source_pos_index is not None:
+ k_sim.p_source_pos_index = np.squeeze(k_sim.p_source_pos_index) - int(1)
+
+ if hasattr(k_sim, 's_source_sig_index') and k_sim.s_source_sig_index is not None:
+ k_sim.s_source_sig_index = np.squeeze(k_sim.s_source_sig_index) - int(1)
+
+ if hasattr(k_sim, 'u_source_sig_index') and k_sim.u_source_sig_index is not None:
+ k_sim.u_source_sig_index = np.squeeze(k_sim.u_source_sig_index) - int(1)
+
+ if hasattr(k_sim, 'p_source_sig_index') and k_sim.p_source_sig_index is not None:
+ k_sim.p_source_sig_index = np.squeeze(k_sim.p_source_sig_index) - int(1)
+
+ # # =========================================================================
+ # # PREPARE VISUALISATIONS
+ # # =========================================================================
+
+ # # pre-compute suitable axes scaling factor
+ # if options.plot_layout or options.plot_sim
+ # [x_sc, scale, prefix] = scaleSI(max(kgrid.x)); ##ok
+ # end
+
+ # # run subscript to plot the simulation layout if 'PlotLayout' is set to true
+ # if options.plot_layout
+ # kspaceFirstOrder_plotLayout;
+ # end
+
+ # # initialise the figure used for animation if 'PlotSim' is set to 'true'
+ # if options.plot_sim
+ # kspaceFirstOrder_initialiseFigureWindow;
+ # end
+
+ # # initialise movie parameters if 'RecordMovie' is set to 'true'
+ # if options.record_movie
+ # kspaceFirstOrder_initialiseMovieParameters;
+ # end
+
+ # =========================================================================
+ # LOOP THROUGH TIME STEPS
+ # =========================================================================
+
+ # update command line status
+ t0 = timer.toc()
+ t0_scale = scale_time(t0)
+ print('\tprecomputation completed in', t0_scale)
+ print('\tstarting time loop...')
+
+ # start time loop
+ for t_index in tqdm(np.arange(index_start, index_end, index_step, dtype=int)):
+
+ # print("0.", np.shape(p))
+
+ # enforce time reversal bounday condition
+ # if options.time_rev:
+ # # load pressure value and enforce as a Dirichlet boundary condition
+ # p[k_sim.sensor_mask_index] = sensor.time_reversal_boundary_data[:, t_index]
+ # # update p_k
+ # p_k = scipy.fft.fft(p)
+ # # compute rhox using an adiabatic equation of state
+ # rhox_mod = p / c0**2
+ # rhox[k_sim.sensor_mask_index] = rhox_mod[k_sim.sensor_mask_index]
+
+
+ # print("1.", np.shape(p))
+
+ # calculate ux at the next time step using dp/dx at the current time step
+ if not options.nonuniform_grid and not options.use_finite_difference:
+
+ if verbose:
+ print("Here 1-----.", np.shape(pml_x), np.shape(pml_x_sgx), np.shape(ux_sgx),
+ '......', np.shape(ddx_k), np.shape(ddx_k_shift_pos), np.shape(kappa),
+ ',,,,,,', np.shape(p_k), np.shape(rho0_sgx_inv))
+
+
+
+ # calculate gradient using the k-space method on a regular grid
+ ux_sgx = pml_x_sgx * (pml_x_sgx * ux_sgx -
+ dt * rho0_sgx_inv * np.real(scipy.fft.ifftn(ddx_k * ddx_k_shift_pos * kappa * p_k, axes=(0,))))
+
+ elif options.use_finite_difference:
+ print("\t\tEXIT! options.use_finite_difference")
+ match options.use_finite_difference:
+ case 2:
+
+ # calculate gradient using second-order accurate finite
+ # difference scheme (including half step forward)
+ dpdx = (np.append(p[1:], 0.0) - p) / kgrid.dx
+ # dpdx = ([p(2:end); 0] - p) / kgrid.dx;
+ ux_sgx = pml_x_sgx * (pml_x_sgx * ux_sgx - dt * rho0_sgx_inv * dpdx )
+
+ case 4:
+
+ # calculate gradient using fourth-order accurate finite
+ # difference scheme (including half step forward)
+ # dpdx = ([0; p(1:(end-1))] - 27*p + 27*[p(2:end); 0] - [p(3:end); 0; 0])/(24*kgrid.dx);
+ dpdx = (np.insert(p[:-1], 0, 0) - 27.0 * p + 27 * np.append(p[1:], 0.0) - np.append(p[2:], [0, 0])) / (24.0 * kgrid.dx)
+ ux_sgx = pml_x_sgx * (pml_x_sgx * ux_sgx - dt * rho0_sgx_inv * dpdx )
+
+ else:
+ print("\t\tEXIT! else", )
+
+ # calculate gradient using the k-space method on a non-uniform grid
+ # via the mapped pseudospectral method
+ ux_sgx = pml_x_sgx * (pml_x_sgx * ux_sgx -
+ dt * rho0_sgx_inv * k_sim.kgrid.dxudxn_sgx * np.real(scipy.fft.ifft(ddx_k * ddx_k_shift_pos * kappa * p_k)) )
+
+
+ # print("2.", np.shape(p))
+
+ # # add in the velocity source term
+ # if (k_sim.source_ux is not False and t_index < np.shape(source.ux)[1]):
+ # #if options.source_ux >= t_index:
+ # match source.u_mode:
+ # case 'dirichlet':
+ # # enforce the source values as a dirichlet boundary condition
+ # ux_sgx[k_sim.u_source_pos_index] = source.ux[k_sim.u_source_sig_index, t_index]
+ # case 'additive':
+ # # extract the source values into a matrix
+ # source_mat = np.zeros([kgrid.Nx, 1])
+ # source_mat[k_sim.u_source_pos_index] = source.ux[k_sim.u_source_sig_index, t_index]
+ # # apply the k-space correction
+ # source_mat = np.real(scipy.fft.ifft(source_kappa * scipy.fft.fft(source_mat)))
+ # # add the source values to the existing field values including the k-space correction
+ # ux_sgx = ux_sgx + source_mat
+ # case 'additive-no-correction':
+ # # add the source values to the existing field values
+ # ux_sgx[k_sim.u_source_pos_index] = ux_sgx[k_sim.u_source_pos_index] + source.ux[k_sim.u_source_sig_index, t_index]
+
+
+ # print("3.", np.shape(p))
+
+ # calculate du/dx at the next time step
+ if not options.nonuniform_grid and not options.use_finite_difference:
+
+ if verbose:
+ print("Here 1.", np.shape(p), np.shape(ux_sgx), np.shape(ddx_k), np.shape(ddx_k_shift_neg), np.shape(kappa))
+
+ # calculate gradient using the k-space method on a regular grid
+ duxdx = np.real(scipy.fft.ifftn(ddx_k * ddx_k_shift_neg * kappa * scipy.fft.fftn(ux_sgx, axes=(0,)), axes=(0,) ) )
+
+ if verbose:
+ print("Here 1(end). duxdx:", np.shape(duxdx))
+
+ elif options.use_finite_difference:
+ print("\t\tEXIT! options.use_finite_difference")
+ match options.use_finite_difference:
+ case 2:
+
+ # calculate gradient using second-order accurate finite difference scheme (including half step backward)
+ # duxdx = (ux_sgx - [0; ux_sgx(1:end - 1)]) / kgrid.dx;
+ duxdx = (ux_sgx - np.append(ux_sgx[:-1], 0)) / kgrid.dx
+
+ case 4:
+
+ # calculate gradient using fourth-order accurate finite difference scheme (including half step backward)
+ duxdx = (np.append([0, 0], ux_sgx[:-2]) - 27.0 * np.append(0, ux_sgx[:-1]) + 27.0 * ux_sgx - np.append(ux_sgx[1:], 0)) / (24.0 * kgrid.dx)
+ # duxdx = ([0; 0; ux_sgx(1:(end - 2))] - 27 * [0; ux_sgx(1:(end - 1))] + 27 * ux_sgx - [ux_sgx(2:end); 0]) / (24 * kgrid.dx);
+
+ else:
+ # calculate gradients using a non-uniform grid via the mapped
+ # pseudospectral method
+ duxdx = kgrid.dxudxn * np.real(scipy.fft.ifftn(ddx_k * ddx_k_shift_neg * kappa * scipy.fft.fftn(ux_sgx, axes=(0,)), axes=(0,)))
+
+
+ # print("4.", np.shape(p))
+
+ # calculate rhox at the next time step
+ if not k_sim.is_nonlinear:
+ # use linearised mass conservation equation
+
+ if verbose:
+ print("pre:", pml_x.shape, rhox.shape, duxdx.shape, dt, rho0.shape)
+
+ rhox = pml_x * (pml_x * rhox - dt * rho0 * duxdx)
+
+ if verbose:
+ print("post:", pml_x.shape, rhox.shape, duxdx.shape, dt, rho0.shape)
+
+ else:
+ # use nonlinear mass conservation equation (explicit calculation)
+ rhox = pml_x * (pml_x * rhox - dt * (2.0 * rhox + rho0) * duxdx)
+
+ # print("5.", np.shape(p))
+
+ # add in the pre-scaled pressure source term as a mass source
+ # if options.source_p >= t_index:
+ # if (k_sim.source_p is not False and t_index < np.shape(source.p)[1]):
+ # print("??????????")
+ # match source.p_mode:
+ # case 'dirichlet':
+ # # enforce source values as a dirichlet boundary condition
+ # rhox[k_sim.p_source_pos_index] = source.p[k_sim.p_source_sig_index, t_index]
+ # case 'additive':
+ # # extract the source values into a matrix
+ # source_mat = np.zeros((kgrid.Nx, 1), dtype=myType)
+ # source_mat[k_sim.p_source_pos_index] = source.p[k_sim.p_source_sig_index, t_index]
+ # # apply the k-space correction
+ # source_mat = np.real(scipy.fft.ifft(source_kappa * scipy.fft.fft(source_mat)))
+ # # add the source values to the existing field values
+ # # including the k-space correction
+ # rhox = rhox + source_mat
+ # case 'additive-no-correction':
+ # # add the source values to the existing field values
+ # rhox[k_sim.p_source_pos_index] = rhox[k_sim.p_source_pos_index] + source.p[k_sim.p_source_sig_index, t_index]
+
+
+ # print("6.", np.shape(p))
+
+ # equation of state
+ if not k_sim.is_nonlinear:
+ # print("is linear", k_sim.equation_of_state, type(k_sim.equation_of_state))
+ match k_sim.equation_of_state:
+ case 'lossless':
+
+ # print("Here 2. lossless / linear", np.shape(p))
+
+ # calculate p using a linear adiabatic equation of state
+ p = np.squeeze(c0**2) * np.squeeze(rhox)
+
+ # print("3.", np.shape(p), np.squeeze(c0**2).shape, np.squeeze(rhox).shape)
+
+ case 'absorbing':
+
+ # print("Here 2. absorbing / linear", np.shape(p))
+
+ # calculate p using a linear absorbing equation of state
+ p = np.squeeze(c0**2 * (rhox
+ + medium.absorb_tau * np.real(scipy.fft.ifftn(medium.absorb_nabla1 * scipy.fft.fftn(rho0 * duxdx, axes=(0,)), axes=(0,) ))
+ - medium.absorb_eta * np.real(scipy.fft.ifftn(medium.absorb_nabla2 * scipy.fft.fftn(rhox, axes=(0,)), axes=(0,))) ) )
+
+
+ case 'stokes':
+
+ # print("Here 2. stokes / linear")
+
+ # calculate p using a linear absorbing equation of state
+ # assuming alpha_power = 2
+ p = c0**2 * (rhox + medium.absorb_tau * rho0 * duxdx)
+
+
+ else:
+ match k_sim.equation_of_state:
+ case 'lossless':
+
+ print("Here 2. lossless / nonlinear")
+
+ # calculate p using a nonlinear adiabatic equation of state
+ p = c0**2 * (rhox + medium.BonA * rhox**2 / (2.0 * rho0))
+
+ case 'absorbing':
+
+ print("Here 2. absorbing / nonlinear")
+
+ # calculate p using a nonlinear absorbing equation of state
+ p = c0**2 * ( rhox
+ + medium.absorb_tau * np.real(scipy.fft.ifftn(medium.absorb_nabla1 * scipy.fft.fftn(rho0 * duxdx, axes=(0,)), axes=(0,)))
+ - medium.absorb_eta * np.real(scipy.fft.ifftn(medium.absorb_nabla2 * scipy.fft.fftn(rhox, axes=(0,)), axes=(0,)))
+ + medium.BonA * rhox**2 / (2.0 * rho0) )
+
+ case 'stokes':
+
+ print("Here 2. stokes / nonlinear")
+
+ # calculate p using a nonlinear absorbing equation of state
+ # assuming alpha_power = 2
+ p = c0**2 * (rhox
+ + medium.absorb_tau * rho0 * duxdx
+ + medium.BonA * rhox**2 / (2.0 * rho0) )
+
+
+ # print("7.", np.shape(p), k_sim.source.p0.shape)
+
+ # enforce initial conditions if source.p0 is defined instead of time varying sources
+ if t_index == 0 and k_sim.source_p0:
+
+ # print(np.shape(rhox))
+
+ if k_sim.source.p0.ndim == 1:
+ p0 = k_sim.source.p0[:, np.newaxis]
+ else:
+ p0 = k_sim.source.p0
+
+ # add the initial pressure to rho as a mass source
+ p = p0
+ rhox = p0 / c0**2
+
+ # compute u(t = t1 - dt/2) based on u(dt/2) = -u(-dt/2) which forces u(t = t1) = 0
+ if not options.use_finite_difference:
+
+ # calculate gradient using the k-space method on a regular grid
+ ux_sgx = dt * rho0_sgx_inv * np.real(scipy.fft.ifftn(ddx_k * ddx_k_shift_pos * kappa * scipy.fft.fftn(p, axes=(0,)), axes=(0,) )) / 2.0
+
+ p_k = scipy.fft.fftn(p, axes=(0,))
+
+
+ else:
+ match options.use_finite_difference:
+ case 2:
+
+ # calculate gradient using second-order accurate finite difference scheme (including half step forward)
+ # dpdx = ([p(2:end); 0] - p) / kgrid.dx;
+
+ dpdx = (np.append(p[1:], 0.0) - p) / kgrid.dx
+ ux_sgx = dt * rho0_sgx_inv * dpdx / 2.0
+
+ case 4:
+
+ # calculate gradient using fourth-order accurate finite difference scheme (including half step backward)
+ # dpdx = ([p(3:end); 0; 0] - 27 * [p(2:end); 0] + 27 * p - [0; p(1:(end-1))]) / (24 * kgrid.dx)
+ dpdx = (np.append(p[2:], [0, 0]) - 27.0 * np.append(p[1:], 0) + 27.0 * p - np.append(0, p[:-1])) / (24.0 * kgrid.dx)
+ ux_sgx = dt * rho0_sgx_inv * dpdx / 2.0
+
+
+ else:
+ # precompute fft of p here so p can be modified for visualisation
+ p_k = scipy.fft.fftn(p, axes=(0,))
+ p_k = p_k[:, np.newaxis]
+
+ # extract required sensor data from the pressure and particle velocity
+ # fields if the number of time steps elapsed is greater than
+ # sensor.record_start_index (defaults to 1)
+ if options.use_sensor and not options.time_rev and (t_index >= sensor.record_start_index):
+
+ # update index for data storage
+ file_index: int = t_index - sensor.record_start_index
+
+ # run sub-function to extract the required data
+ extract_options = dotdict({'record_u_non_staggered': k_sim.record.u_non_staggered,
+ 'record_u_split_field': k_sim.record.u_split_field,
+ 'record_I': k_sim.record.I,
+ 'record_I_avg': k_sim.record.I_avg,
+ 'binary_sensor_mask': k_sim.binary_sensor_mask,
+ 'record_p': k_sim.record.p,
+ 'record_p_max': k_sim.record.p_max,
+ 'record_p_min': k_sim.record.p_min,
+ 'record_p_rms': k_sim.record.p_rms,
+ 'record_p_max_all': k_sim.record.p_max_all,
+ 'record_p_min_all': k_sim.record.p_min_all,
+ 'record_u': k_sim.record.u,
+ 'record_u_max': k_sim.record.u_max,
+ 'record_u_min': k_sim.record.u_min,
+ 'record_u_rms': k_sim.record.u_rms,
+ 'record_u_max_all': k_sim.record.u_max_all,
+ 'record_u_min_all': k_sim.record.u_min_all,
+ 'compute_directivity': False})
+
+ # run sub-function to extract the required data from the acoustic variables
+ sensor_data = extract_sensor_data(kgrid.dim, sensor_data, file_index, k_sim.sensor_mask_index, extract_options, record, p, ux_sgx)
+
+
+
+ return sensor_data
+
+
+
+
+
diff --git a/kwave/ktransducer.py b/kwave/ktransducer.py
index 176e3f053..83839e8fb 100644
--- a/kwave/ktransducer.py
+++ b/kwave/ktransducer.py
@@ -644,7 +644,11 @@ def delay_mask(self, mode=None):
].min() # -1s compatibility
else:
mask[unflatten_matlab_mask(mask, active_elements_index - 1)] += self.stored_beamforming_delays_offset # -1s compatibility
- return mask.astype(np.uint8)
+
+ # returns a unsigned int mask now.
+ int_mask = mask.astype(np.uint8).copy()
+
+ return int_mask
@property
def elevation_beamforming_delays(self):
diff --git a/kwave/options/simulation_options.py b/kwave/options/simulation_options.py
index 78a49f104..b68f294db 100644
--- a/kwave/options/simulation_options.py
+++ b/kwave/options/simulation_options.py
@@ -4,7 +4,7 @@
from dataclasses import dataclass, field
from enum import Enum
from tempfile import gettempdir
-from typing import List, Optional, TYPE_CHECKING
+from typing import List, Optional, TYPE_CHECKING, Union
import numpy as np
@@ -82,6 +82,7 @@ class SimulationOptions(object):
pml_x_size: PML Size for x-axis
pml_y_size: PML Size for y-axis
pml_z_size: PML Size for z-axis
+ kelvin_voigt_model: setting for elastic code
"""
simulation_type: SimulationType = SimulationType.FLUID
@@ -118,6 +119,17 @@ class SimulationOptions(object):
pml_x_size: Optional[int] = None
pml_y_size: Optional[int] = None
pml_z_size: Optional[int] = None
+ kelvin_voigt_model: bool = True
+ time_rev: bool = False
+
+ use_sensor: Optional[Union[int, bool]] = None
+
+ blank_sensor: Optional[bool] = None
+ cuboid_corners: Optional[Union[bool, np.ndarray]] = None
+ nonuniform_grid: Optional[bool] = None
+ elastic_time_rev: Optional[bool] = None
+ binary_sensor_mask: Optional[bool] = None
+
def __post_init__(self):
assert self.cartesian_interp in [
@@ -327,6 +339,7 @@ def option_factory(kgrid: "kWaveGrid", options: SimulationOptions):
if options.use_fd:
# input only supported in 1D fluid code
assert kgrid.dim == 1 and not options.simulation_type.is_elastic_simulation(), "Optional input ''use_fd'' only supported in 1D."
+
# get optimal pml size
if options.simulation_type.is_axisymmetric() or options.pml_auto:
if options.simulation_type.is_axisymmetric():
@@ -347,4 +360,5 @@ def option_factory(kgrid: "kWaveGrid", options: SimulationOptions):
# cleanup unused variables
del pml_size_temp
+
return options
diff --git a/kwave/pstdElastic2D.py b/kwave/pstdElastic2D.py
new file mode 100644
index 000000000..17eb63e24
--- /dev/null
+++ b/kwave/pstdElastic2D.py
@@ -0,0 +1,1050 @@
+import numpy as np
+from scipy.interpolate import interpn
+import scipy.fft
+from tqdm import tqdm
+from typing import Union
+from copy import deepcopy
+
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksensor import kSensor
+from kwave.ksource import kSource
+from kwave.kWaveSimulation import kWaveSimulation
+
+from kwave.ktransducer import NotATransducer
+
+from kwave.utils.conversion import db2neper
+from kwave.utils.data import scale_time
+# from kwave.utils.data import scale_SI
+from kwave.utils.filters import gaussian_filter
+# from kwave.utils.matlab import rem
+from kwave.utils.pml import get_pml
+from kwave.utils.signals import reorder_sensor_data
+from kwave.utils.tictoc import TicToc
+from kwave.utils.dotdictionary import dotdict
+
+from kwave.options.simulation_options import SimulationOptions
+
+from kwave.kWaveSimulation_helper import extract_sensor_data
+
+def nan_helper(y):
+ """Helper to handle indices and logical indices of NaNs.
+
+ Input:
+ - y, 1d numpy array with possible NaNs
+ Output:
+ - nans, logical indices of NaNs
+ - index, a function, with signature indices= index(logical_indices),
+ to convert logical indices of NaNs to 'equivalent' indices
+ Example:
+ >>> # linear interpolation of NaNs
+ >>> nans, x= nan_helper(y)
+ >>> y[nans]= np.interp(x(nans), x(~nans), y[~nans])
+ """
+
+ return np.isnan(y), lambda z: z.nonzero()[0]
+
+def pstd_elastic_2d(kgrid: kWaveGrid,
+ source: kSource,
+ sensor: Union[NotATransducer, kSensor],
+ medium: kWaveMedium,
+ simulation_options: SimulationOptions,
+ verbose: bool = False):
+ """
+ 2D time-domain simulation of elastic wave propagation.
+
+ DESCRIPTION:
+ pstd_elastic_2d simulates the time-domain propagation of elastic waves
+ through a two-dimensional homogeneous or heterogeneous medium given
+ four input structures: kgrid, medium, source, and sensor. The
+ computation is based on a pseudospectral time domain model which
+ accounts for viscoelastic absorption and heterogeneous material
+ parameters. At each time-step (defined by kgrid.dt and kgrid.Nt or
+ kgrid.t_array), the wavefield parameters at the positions defined by
+ sensor.mask are recorded and stored. If kgrid.t_array is set to
+ 'auto', this array is automatically generated using the makeTime
+ method of the kWaveGrid class. An anisotropic absorbing boundary
+ layer called a perfectly matched layer (PML) is implemented to
+ prevent waves that leave one side of the domain being reintroduced
+ from the opposite side (a consequence of using the FFT to compute the
+ spatial derivatives in the wave equation). This allows infinite
+ domain simulations to be computed using small computational grids.
+
+ An initial pressure distribution can be specified by assigning a
+ matrix of pressure values the same size as the computational grid to
+ source.p0. This is then assigned to the normal components of the
+ stress within the simulation function. A time varying stress source
+ can similarly be specified by assigning a binary matrix (i.e., a
+ matrix of 1's and 0's with the same dimensions as the computational
+ grid) to source.s_mask where the 1's represent the grid points that
+ form part of the source. The time varying input signals are then
+ assigned to source.sxx, source.syy, and source.sxy. These can be a
+ single time series (in which case it is applied to all source
+ elements), or a matrix of time series following the source elements
+ using MATLAB's standard column-wise linear matrix index ordering. A
+ time varying velocity source can be specified in an analogous
+ fashion, where the source location is specified by source.u_mask, and
+ the time varying input velocity is assigned to source.ux and
+ source.uy.
+
+ The field values are returned as arrays of time series at the sensor
+ locations defined by sensor.mask. This can be defined in three
+ different ways. (1) As a binary matrix (i.e., a matrix of 1's and 0's
+ with the same dimensions as the computational grid) representing the
+ grid points within the computational grid that will collect the data.
+ (2) As the grid coordinates of two opposing corners of a rectangle in
+ the form [x1; y1; x2; y2]. This is equivalent to using a binary
+ sensor mask covering the same region, however, the output is indexed
+ differently as discussed below. (3) As a series of Cartesian
+ coordinates within the grid which specify the location of the
+ pressure values stored at each time step. If the Cartesian
+ coordinates don't exactly match the coordinates of a grid point, the
+ output values are calculated via interpolation. The Cartesian points
+ must be given as a 2 by N matrix corresponding to the x and y
+ positions, respectively, where the Cartesian origin is assumed to be
+ in the center of the grid. If no output is required, the sensor input
+ can be replaced with an empty array [].
+
+ If sensor.mask is given as a set of Cartesian coordinates, the
+ computed sensor_data is returned in the same order. If sensor.mask is
+ given as a binary matrix, sensor_data is returned using MATLAB's
+ standard column-wise linear matrix index ordering. In both cases, the
+ recorded data is indexed as sensor_data(sensor_point_index,
+ time_index). For a binary sensor mask, the field values at a
+ particular time can be restored to the sensor positions within the
+ computation grid using unmaskSensorData. If sensor.mask is given as a
+ list of opposing corners of a rectangle, the recorded data is indexed
+ as sensor_data(rect_index).p(x_index, y_index, time_index), where
+ x_index and y_index correspond to the grid index within the
+ rectangle, and rect_index corresponds to the number of rectangles if
+ more than one is specified.
+
+ By default, the recorded acoustic pressure field is passed directly
+ to the output sensor_data. However, other acoustic parameters can
+ also be recorded by setting sensor.record to a cell array of the form
+ {'p', 'u', 'p_max', ...}. For example, both the particle velocity and
+ the acoustic pressure can be returned by setting sensor.record =
+ {'p', 'u'}. If sensor.record is given, the output sensor_data is
+ returned as a structure with the different outputs appended as
+ structure fields. For example, if sensor.record = {'p', 'p_final',
+ 'p_max', 'u'}, the output would contain fields sensor_data.p,
+ sensor_data.p_final, sensor_data.p_max, sensor_data.ux, and
+ sensor_data.uy. Most of the output parameters are recorded at the
+ given sensor positions and are indexed as
+ sensor_data.field(sensor_point_index, time_index) or
+ sensor_data(rect_index).field(x_index, y_index, time_index) if using
+ a sensor mask defined as opposing rectangular corners. The exceptions
+ are the averaged quantities ('p_max', 'p_rms', 'u_max', 'p_rms',
+ 'I_avg'), the 'all' quantities ('p_max_all', 'p_min_all',
+ 'u_max_all', 'u_min_all'), and the final quantities ('p_final',
+ 'u_final'). The averaged quantities are indexed as
+ sensor_data.p_max(sensor_point_index) or
+ sensor_data(rect_index).p_max(x_index, y_index) if using rectangular
+ corners, while the final and 'all' quantities are returned over the
+ entire grid and are always indexed as sensor_data.p_final(nx, ny),
+ regardless of the type of sensor mask.
+
+ pstd_elastic_2d may also be used for time reversal image reconstruction
+ by assigning the time varying pressure recorded over an arbitrary
+ sensor surface to the input field sensor.time_reversal_boundary_data.
+ This data is then enforced in time reversed order as a time varying
+ Dirichlet boundary condition over the sensor surface given by
+ sensor.mask. The boundary data must be indexed as
+ sensor.time_reversal_boundary_data(sensor_point_index, time_index).
+ If sensor.mask is given as a set of Cartesian coordinates, the
+ boundary data must be given in the same order. An equivalent binary
+ sensor mask (computed using nearest neighbour interpolation) is then
+ used to place the pressure values into the computational grid at each
+ time step. If sensor.mask is given as a binary matrix of sensor
+ points, the boundary data must be ordered using MATLAB's standard
+ column-wise linear matrix indexing. If no additional inputs are
+ required, the source input can be replaced with an empty array [].
+
+ USAGE:
+ sensor_data = pstd_elastic_2d(kWaveGrid, kWaveMedium, kSource, kSensor)
+
+
+ INPUTS:
+ The minimum fields that must be assigned to run an initial value problem
+ (for example, a photoacoustic forward simulation) are marked with a *.
+
+ kgrid* - k-Wave grid object returned by kWaveGrid
+ containing Cartesian and k-space grid fields
+ kgrid.t_array* - evenly spaced array of time values [s] (set
+ to 'auto' by kWaveGrid)
+
+ medium.sound_speed_compression*
+ - compressional sound speed distribution
+ within the acoustic medium [m/s]
+ medium.sound_speed_shear*
+ - shear sound speed distribution within the
+ acoustic medium [m/s]
+ medium.density* - density distribution within the acoustic
+ medium [kg/m^3]
+ medium.alpha_coeff_compression
+ - absorption coefficient for compressional
+ waves [dB/(MHz^2 cm)]
+ medium.alpha_coeff_shear
+ - absorption coefficient for shear waves
+ [dB/(MHz^2 cm)]
+
+ source.p0* - initial pressure within the acoustic medium
+ source.sxx - time varying stress at each of the source
+ positions given by source.s_mask
+ source.syy - time varying stress at each of the source
+ positions given by source.s_mask
+ source.sxy - time varying stress at each of the source
+ positions given by source.s_mask
+ source.s_mask - binary matrix specifying the positions of
+ the time varying stress source distributions
+ source.s_mode - optional input to control whether the input
+ stress is injected as a mass source or
+ enforced as a dirichlet boundary condition;
+ valid inputs are 'additive' (the default) or
+ 'dirichlet'
+ source.ux - time varying particle velocity in the
+ x-direction at each of the source positions
+ given by source.u_mask
+ source.uy - time varying particle velocity in the
+ y-direction at each of the source positions
+ given by source.u_mask
+ source.u_mask - binary matrix specifying the positions of
+ the time varying particle velocity
+ distribution
+ source.u_mode - optional input to control whether the input
+ velocity is applied as a force source or
+ enforced as a dirichlet boundary condition;
+ valid inputs are 'additive' (the default) or
+ 'dirichlet'
+
+ sensor.mask* - binary matrix or a set of Cartesian points
+ where the pressure is recorded at each
+ time-step
+ sensor.record - cell array of the acoustic parameters to
+ record in the form sensor.record = {'p',
+ 'u', ...}; valid inputs are:
+
+ - 'p' (acoustic pressure)
+ - 'p_max' (maximum pressure)
+ - 'p_min' (minimum pressure)
+ - 'p_rms' (RMS pressure)
+ - 'p_final' (final pressure field at all grid points)
+ - 'p_max_all' (maximum pressure at all grid points)
+ - 'p_min_all' (minimum pressure at all grid points)
+ - 'u' (particle velocity)
+ - 'u_max' (maximum particle velocity)
+ - 'u_min' (minimum particle velocity)
+ - 'u_rms' (RMS particle21st January 2014 velocity)
+ - 'u_final' (final particle velocity field at all grid points)
+ - 'u_max_all' (maximum particle velocity at all grid points)
+ - 'u_min_all' (minimum particle velocity at all grid points)
+ - 'u_non_staggered' (particle velocity on non-staggered grid)
+ - 'u_split_field' (particle velocity on non-staggered grid split
+ into compressional and shear components)
+ - 'I' (time varying acoustic intensity)
+ - 'I_avg' (average acoustic intensity)
+
+ NOTE: the acoustic pressure outputs are calculated from the
+ normal stress via: p = -(sxx + syy) / 2
+
+ sensor.record_start_index
+ - time index at which the sensor should start
+ recording the data specified by
+ sensor.record (default = 1, but shifted to 0)
+ sensor.time_reversal_boundary_data
+ - time varying pressure enforced as a
+ Dirichlet boundary condition over sensor.mask
+
+ Note: For a heterogeneous medium, medium.sound_speed_compression,
+ medium.sound_speed_shear, and medium.density must be given in matrix form
+ with the same dimensions as kgrid. For a homogeneous medium, these can be
+ given as scalar values.
+
+ OPTIONAL INPUTS:
+ Optional 'string', value pairs that may be used to modify the default
+ computational settings.
+
+ See .html help file for details.
+
+ OUTPUTS:
+ If sensor.record is not defined by the user:
+ sensor_data - time varying pressure recorded at the sensor
+ positions given by sensor.mask
+
+ If sensor.record is defined by the user:
+ sensor_data.p - time varying pressure recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'p' is set)
+ sensor_data.p_max - maximum pressure recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'p_max' is set)
+ sensor_data.p_min - minimum pressure recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'p_min' is set)
+ sensor_data.p_rms - rms of the time varying pressure recorded at
+ the sensor positions given by sensor.mask
+ (returned if 'p_rms' is set)
+ sensor_data.p_final - final pressure field at all grid points
+ within the domain (returned if 'p_final' is
+ set)
+ sensor_data.p_max_all - maximum pressure recorded at all grid points
+ within the domain (returned if 'p_max_all'
+ is set)
+ sensor_data.p_min_all - minimum pressure recorded at all grid points
+ within the domain (returned if 'p_min_all'
+ is set)
+ sensor_data.ux - time varying particle velocity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'u' is
+ set)
+ sensor_data.uy - time varying particle velocity in the
+ y-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'u' is
+ set)
+ sensor_data.ux_max - maximum particle velocity in the x-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_max' is set)
+ sensor_data.uy_max - maximum particle velocity in the y-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_max' is set)
+ sensor_data.ux_min - minimum particle velocity in the x-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_min' is set)
+ sensor_data.uy_min - minimum particle velocity in the y-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_min' is set)
+ sensor_data.ux_rms - rms of the time varying particle velocity in
+ the x-direction recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_rms' is set)
+ sensor_data.uy_rms - rms of the time varying particle velocity in
+ the y-direction recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_rms' is set)
+ sensor_data.ux_final - final particle velocity field in the
+ x-direction at all grid points within the
+ domain (returned if 'u_final' is set)
+ sensor_data.uy_final - final particle velocity field in the
+ y-direction at all grid points within the
+ domain (returned if 'u_final' is set)
+ sensor_data.ux_max_all - maximum particle velocity in the x-direction
+ recorded at all grid points within the
+ domain (returned if 'u_max_all' is set)
+ sensor_data.uy_max_all - maximum particle velocity in the y-direction
+ recorded at all grid points within the
+ domain (returned if 'u_max_all' is set)
+ sensor_data.ux_min_all - minimum particle velocity in the x-direction
+ recorded at all grid points within the
+ domain (returned if 'u_min_all' is set)
+ sensor_data.uy_min_all - minimum particle velocity in the y-direction
+ recorded at all grid points within the
+ domain (returned if 'u_min_all' is set)
+ sensor_data.ux_non_staggered
+ - time varying particle velocity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask after shifting to the
+ non-staggered grid (returned if
+ 'u_non_staggered' is set)
+ sensor_data.uy_non_staggered
+ - time varying particle velocity in the
+ y-direction recorded at the sensor positions
+ given by sensor.mask after shifting to the
+ non-staggered grid (returned if
+ 'u_non_staggered' is set)
+ sensor_data.ux_split_p - compressional component of the time varying
+ particle velocity in the x-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.ux_split_s - shear component of the time varying particle
+ velocity in the x-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.uy_split_p - compressional component of the time varying
+ particle velocity in the y-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.uy_split_s - shear component of the time varying particle
+ velocity in the y-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.Ix - time varying acoustic intensity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I' is
+ set)
+ sensor_data.Iy - time varying acoustic intensity in the
+ y-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I' is
+ set)
+ sensor_data.Ix_avg - average acoustic intensity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I_avg' is
+ set)
+ sensor_data.Iy_avg - average acoustic intensity in the
+ y-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I_avg' is
+ set)
+
+ ABOUT:
+ author - Bradley Treeby & Ben Cox
+ date - 11th March 2013
+ last update - 13th January 2019
+
+ This function is part of the k-Wave Toolbox (http://www.k-wave.org)
+ Copyright (C) 2013-2019 Bradley Treeby and Ben Cox
+
+ See also kspaceFirstOrder2D, kWaveGrid, pstdElastic3D
+
+ This file is part of k-Wave. k-Wave is free software: you can
+ redistribute it and/or modify it under the terms of the GNU Lesser
+ General Public License as published by the Free Software Foundation,
+ either version 3 of the License, or (at your option) any later version.
+
+ k-Wave is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with k-Wave. If not, see .
+ """
+
+ # =========================================================================
+ # CHECK INPUT STRUCTURES AND OPTIONAL INPUTS
+ # =========================================================================
+
+ # start the timer and store the start time
+ timer = TicToc()
+ timer.tic()
+
+ # run script to check inputs and create the required arrays
+ k_sim = kWaveSimulation(kgrid=kgrid, source=source, sensor=sensor, medium=medium,
+ simulation_options=simulation_options)
+
+ # this will create the sensor_data dotdict
+ k_sim.input_checking("pstd_elastic_2d")
+
+ sensor_data = k_sim.sensor_data
+ options = k_sim.options
+
+ # =========================================================================
+ # CALCULATE MEDIUM PROPERTIES ON STAGGERED GRID
+ # =========================================================================
+
+ rho0 = np.atleast_1d(k_sim.rho0)
+
+ m_rho0: int = np.squeeze(k_sim.rho0).ndim
+
+ # assign the lame parameters
+ mu = medium.density * np.power(medium.sound_speed_shear, 2)
+ lame_lambda = medium.density * medium.sound_speed_compression**2 - 2.0 * mu
+ m_mu: int = np.squeeze(mu).ndim
+
+ # assign the viscosity coefficients
+ if options.kelvin_voigt_model:
+ eta = 2.0 * rho0 * medium.sound_speed_shear**3 * db2neper(deepcopy(medium.alpha_coeff_shear), 2.0)
+ chi = 2.0 * rho0 * medium.sound_speed_compression**3 * db2neper(deepcopy(medium.alpha_coeff_compression), 2.0) - 2.0 * eta
+ m_eta : int = np.squeeze(eta).ndim
+
+ # calculate the values of the density at the staggered grid points
+ # using the arithmetic average [1, 2], where sgx = (x + dx/2, y) and
+ # sgy = (x, y + dy/2)
+
+ points = (np.squeeze(k_sim.kgrid.x_vec), np.squeeze(k_sim.kgrid.y_vec))
+
+ if (m_rho0 == 2 and options.use_sg):
+
+ # rho0 is heterogeneous and staggered grids are used
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec) + k_sim.kgrid.dx / 2,
+ np.squeeze(k_sim.kgrid.y_vec),
+ indexing='ij',)
+ interp_points = np.moveaxis(mg, 0, -1)
+
+ rho0_sgx = interpn(points, k_sim.rho0, interp_points, method='linear', bounds_error=False)
+
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec),
+ np.squeeze(k_sim.kgrid.y_vec) + k_sim.kgrid.dy / 2,
+ indexing='ij',)
+
+ interp_points = np.moveaxis(mg, 0, -1)
+ rho0_sgy = interpn(points, k_sim.rho0, interp_points, method='linear', bounds_error=False)
+
+ rho0_sgx[np.isnan(rho0_sgx)] = k_sim.rho0[np.isnan(rho0_sgx)]
+ rho0_sgy[np.isnan(rho0_sgy)] = k_sim.rho0[np.isnan(rho0_sgy)]
+
+ else:
+
+ # rho0 is homogeneous or staggered grids are not used
+ rho0_sgx = k_sim.rho0
+ rho0_sgy = k_sim.rho0
+
+
+ # invert rho0 so it doesn't have to be done each time step
+ rho0_sgx_inv = 1.0 / rho0_sgx
+ rho0_sgy_inv = 1.0 / rho0_sgy
+
+ # clear unused variables
+ del rho0_sgx
+ del rho0_sgy
+
+
+ # calculate the values of mu at the staggered grid points using the
+ # harmonic average [1, 2], where sgxy = (x + dx/2, y + dy/2)
+ if (m_mu == 2 and options.use_sg):
+
+ # mu is heterogeneous and staggered grids are used
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec) + k_sim.kgrid.dx / 2,
+ np.squeeze(k_sim.kgrid.y_vec) + k_sim.kgrid.dy / 2,
+ indexing='ij',)
+
+ interp_points = np.moveaxis(mg, 0, -1)
+
+ with np.errstate(divide='ignore', invalid='ignore'):
+ mu_sgxy = 1.0 / interpn(points, 1.0 / mu, interp_points, method='linear', bounds_error=False)
+
+ # set values outside of the interpolation range to original values
+ mu_sgxy[np.isnan(mu_sgxy)] = mu[np.isnan(mu_sgxy)]
+
+ else:
+ # mu is homogeneous or staggered grids are not used
+ mu_sgxy = mu
+
+
+ # calculate the values of eta at the staggered grid points using the
+ # harmonic average [1, 2], where sgxy = (x + dx/2, y + dy/2)
+ if options.kelvin_voigt_model:
+ if (m_eta == 2 and options.use_sg):
+
+ # eta is heterogeneous and staggered grids are used
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec) + k_sim.kgrid.dx / 2,
+ np.squeeze(k_sim.kgrid.y_vec) + k_sim.kgrid.dy / 2,
+ indexing ='ij')
+
+ interp_points = np.moveaxis(mg, 0, -1)
+
+ with np.errstate(divide='ignore', invalid='ignore'):
+ eta_sgxy = 1.0 / interpn(points, 1.0 / eta, interp_points, method='linear', bounds_error=False)
+
+ # set values outside of the interpolation range to original values
+ eta_sgxy[np.isnan(eta_sgxy)] = eta[np.isnan(eta_sgxy)]
+
+ else:
+
+ # eta is homogeneous or staggered grids are not used
+ eta_sgxy = eta
+
+ # [1] Moczo, P., Kristek, J., Vavry?uk, V., Archuleta, R. J., & Halada, L.
+ # (2002). 3D heterogeneous staggered-grid finite-difference modeling of
+ # seismic motion with volume harmonic and arithmetic averaging of elastic
+ # moduli and densities. Bulletin of the Seismological Society of America,
+ # 92(8), 3042-3066.
+
+ # [2] Toyoda, M., Takahashi, D., & Kawai, Y. (2012). Averaged material
+ # parameters and boundary conditions for the vibroacoustic
+ # finite-difference time-domain method with a nonuniform mesh. Acoustical
+ # Science and Technology, 33(4), 273-276.
+
+ # =========================================================================
+ # RECORDER
+ # =========================================================================
+
+ record = k_sim.record
+
+ # =========================================================================
+ # PREPARE DERIVATIVE AND PML OPERATORS
+ # =========================================================================
+
+ # get the regular PML operators based on the reference sound speed and PML settings
+ Nx, Ny = k_sim.kgrid.Nx, k_sim.kgrid.Ny
+ dx, dy = k_sim.kgrid.dx, k_sim.kgrid.dy
+ dt = k_sim.kgrid.dt
+ Nt = k_sim.kgrid.Nt
+
+ pml_x_alpha, pml_y_alpha = options.pml_x_alpha, options.pml_y_alpha
+ pml_x_size, pml_y_size = options.pml_x_size, options.pml_y_size
+ c_ref = k_sim.c_ref
+
+ multi_axial_PML_ratio = options.multi_axial_PML_ratio
+
+ pml_x = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, False, 0)
+ pml_x_sgx = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, True, 0)
+ pml_y = get_pml(Ny, dy, dt, c_ref, pml_y_size, pml_y_alpha, False, 1)
+ pml_y_sgy = get_pml(Ny, dy, dt, c_ref, pml_y_size, pml_y_alpha, True, 1)
+
+ # get the multi-axial PML operators
+ mpml_x = get_pml(Nx, dx, dt, c_ref, pml_x_size, multi_axial_PML_ratio * pml_x_alpha, False, 0)
+ mpml_x_sgx = get_pml(Nx, dx, dt, c_ref, pml_x_size, multi_axial_PML_ratio * pml_x_alpha, True, 0)
+ mpml_y = get_pml(Ny, dy, dt, c_ref, pml_y_size, multi_axial_PML_ratio * pml_y_alpha, False, 1)
+ mpml_y_sgy = get_pml(Ny, dy, dt, c_ref, pml_y_size, multi_axial_PML_ratio * pml_y_alpha, True, 1)
+
+ # define the k-space derivative operators, multiply by the staggered
+ # grid shift operators, and then re-order using ifftshift (the option
+ # options.use_sg exists for debugging)
+ kx_vec = np.squeeze(k_sim.kgrid.k_vec[0])
+ ky_vec = np.squeeze(k_sim.kgrid.k_vec[1])
+
+ if options.use_sg:
+ ddx_k_shift_pos = scipy.fft.ifftshift(1j * kx_vec * np.exp(1j * kx_vec * dx / 2.0))
+ ddy_k_shift_pos = scipy.fft.ifftshift(1j * ky_vec * np.exp(1j * ky_vec * dy / 2.0))
+ ddx_k_shift_neg = scipy.fft.ifftshift(1j * kx_vec * np.exp(-1j * kx_vec * dx / 2.0))
+ ddy_k_shift_neg = scipy.fft.ifftshift(1j * ky_vec * np.exp(-1j * ky_vec * dy / 2.0))
+ else:
+ ddx_k_shift_pos = scipy.fft.ifftshift(1j * kx_vec)
+ ddx_k_shift_neg = scipy.fft.ifftshift(1j * kx_vec)
+ ddy_k_shift_pos = scipy.fft.ifftshift(1j * ky_vec)
+ ddy_k_shift_neg = scipy.fft.ifftshift(1j * ky_vec)
+
+ # shape for broadcasting
+ ddx_k_shift_pos = np.expand_dims(ddx_k_shift_pos, axis=1)
+ ddx_k_shift_neg = np.expand_dims(ddx_k_shift_neg, axis=1)
+ ddy_k_shift_pos = np.expand_dims(np.squeeze(ddy_k_shift_pos), axis=0)
+ ddy_k_shift_neg = np.expand_dims(np.squeeze(ddy_k_shift_neg), axis=0)
+
+ # =========================================================================
+ # DATA CASTING
+ # =========================================================================
+
+ # run subscript to cast the remaining loop variables to the data type
+ # specified by data_cast
+ if not (options.data_cast == 'off'):
+ myType = np.single
+ else:
+ myType = np.double
+
+ grid_shape = (Nx, Ny)
+
+ # preallocate the loop variables
+ ux_split_x = np.zeros((Nx, Ny), dtype=myType)
+ ux_split_y = np.zeros((Nx, Ny), dtype=myType)
+ uy_split_x = np.zeros((Nx, Ny), dtype=myType)
+ uy_split_y = np.zeros((Nx, Ny), dtype=myType)
+
+ ux_sgx = np.zeros((Nx, Ny), dtype=myType) # **
+ uy_sgy = np.zeros((Nx, Ny), dtype=myType) # **
+
+ sxx_split_x = np.zeros((Nx, Ny), dtype=myType)
+ sxx_split_y = np.zeros((Nx, Ny), dtype=myType)
+ syy_split_x = np.zeros((Nx, Ny), dtype=myType)
+ syy_split_y = np.zeros((Nx, Ny), dtype=myType)
+ sxy_split_x = np.zeros((Nx, Ny), dtype=myType)
+ sxy_split_y = np.zeros((Nx, Ny), dtype=myType)
+
+ duxdx = np.zeros((Nx, Ny), dtype=myType) # **
+ duxdy = np.zeros((Nx, Ny), dtype=myType) # **
+ duydy = np.zeros((Nx, Ny), dtype=myType) # **
+ duydx = np.zeros((Nx, Ny), dtype=myType) # **
+
+ dsxxdx = np.zeros((Nx, Ny), dtype=myType) # **
+ dsxydy = np.zeros((Nx, Ny), dtype=myType) # **
+ dsxydx = np.zeros((Nx, Ny), dtype=myType) # **
+ dsyydy = np.zeros((Nx, Ny), dtype=myType) # **
+
+ p = np.zeros((Nx, Ny), dtype=myType) # **
+
+ if not (options.data_cast == 'off'):
+ two = np.float32(2.0)
+ dt = np.float32(dt)
+ else:
+ two = np.float64(2.0)
+ dt = np.float64(dt)
+
+ if m_mu == 2:
+ mu = mu.astype(myType)
+ lame_lambda = lame_lambda.astype(myType)
+ else:
+ if not (options.data_cast == 'off'):
+ mu = np.float32(mu)
+ lame_lambda = np.float32(lame_lambda)
+ else:
+ mu = np.float64(mu)
+ lame_lambda = np.float64(lame_lambda)
+
+ if options.kelvin_voigt_model:
+ dduxdxdt = np.zeros(grid_shape, dtype=myType) # **
+ dduydydt = np.zeros(grid_shape, dtype=myType) # **
+ dduxdydt = np.zeros(grid_shape, dtype=myType) # **
+ dduydxdt = np.zeros(grid_shape, dtype=myType) # **
+ if m_eta == 2:
+ eta = eta.astype(myType)
+ chi = chi.astype(myType)
+ else:
+ if not (options.data_cast == 'off'):
+ eta = np.float32(eta)
+ chi = np.float32(chi)
+ else:
+ eta = np.float64(eta)
+ chi = np.float64(chi)
+
+
+
+ # to save memory, the variables noted with a ** do not neccesarily need to
+ # be explicitly stored (they are not needed for update steps). Instead they
+ # could be replaced with a small number of temporary variables that are
+ # reused several times during the time loop.
+
+
+ # =========================================================================
+ # CREATE INDEX VARIABLES
+ # =========================================================================
+
+ # setup the time index variable
+ if (not options.time_rev):
+ index_start: int = 0
+ index_step: int = 1
+ index_end: int = Nt
+ else:
+ # throw error for unsupported feature
+ raise TypeError('Time reversal using sensor.time_reversal_boundary_data is not currently supported.')
+
+
+ # =========================================================================
+ # PREPARE VISUALISATIONS
+ # =========================================================================
+
+ # pre-compute suitable axes scaling factor
+ # if (options.plot_layout or options.plot_sim):
+ # (x_sc, scale, prefix) = scale_SI(np.max([k_sim.kgrid.x_vec, k_sim.kgrid.y_vec]))
+
+ # throw error for currently unsupported plot layout feature
+ # if options.plot_layout:
+ # raise TypeError('PlotLayout input is not currently supported.')
+
+ # initialise the figure used for animation if 'PlotSim' is set to 'True'
+ # if options.plot_sim:
+ # kspaceFirstOrder_initialiseFigureWindow;
+
+ # initialise movie parameters if 'RecordMovie' is set to 'True'
+ # if options.record_movie:
+ # kspaceFirstOrder_initialiseMovieParameters;
+
+
+ # =========================================================================
+ # LOOP THROUGH TIME STEPS
+ # =========================================================================
+
+ # update command line status
+ t0 = timer.toc()
+ t0_scale = scale_time(t0)
+ print('\tprecomputation completed in', t0_scale)
+ print('\tstarting time loop...')
+
+ # end at this point - but nothing is saved to disk.
+ if options.save_to_disk_exit:
+ return
+
+ # consistent sizing for broadcasting
+ pml_x_sgx = np.transpose(pml_x_sgx)
+
+ pml_y_sgy = np.squeeze(pml_y_sgy)
+ pml_y_sgy = np.expand_dims(pml_y_sgy, axis=0)
+
+ mpml_x = np.transpose(mpml_x)
+
+ mpml_y = np.squeeze(mpml_y)
+ mpml_y = np.expand_dims(mpml_y, axis=0)
+
+ mpml_x_sgx = np.transpose(mpml_x_sgx)
+
+ mpml_y_sgy = np.squeeze(mpml_y_sgy)
+ mpml_y_sgy = np.expand_dims(mpml_y_sgy, axis=0)
+
+ pml_x = np.transpose(pml_x)
+
+ pml_y = np.squeeze(pml_y)
+ pml_y = np.expand_dims(pml_y, axis=0)
+
+
+ # These should be zero indexed
+ if hasattr(k_sim, 's_source_sig_index') and k_sim.s_source_pos_index is not None:
+ k_sim.s_source_pos_index = np.squeeze(k_sim.s_source_pos_index) - int(1)
+
+ if hasattr(k_sim, 'u_source_pos_index') and k_sim.u_source_pos_index is not None:
+ k_sim.u_source_pos_index = np.squeeze(k_sim.u_source_pos_index) - int(1)
+
+ if hasattr(k_sim, 'p_source_pos_index') and k_sim.p_source_pos_index is not None:
+ k_sim.p_source_pos_index = np.squeeze(k_sim.p_source_pos_index) - int(1)
+
+ if hasattr(k_sim, 's_source_sig_index') and k_sim.s_source_sig_index is not None:
+ k_sim.s_source_sig_index = np.squeeze(k_sim.s_source_sig_index) - int(1)
+
+ if hasattr(k_sim, 'u_source_sig_index') and k_sim.u_source_sig_index is not None:
+ k_sim.u_source_sig_index = np.squeeze(k_sim.u_source_sig_index) - int(1)
+
+ if hasattr(k_sim, 'p_source_sig_index') and k_sim.p_source_sig_index is not None:
+ k_sim.p_source_sig_index = np.squeeze(k_sim.p_source_sig_index) - int(1)
+
+ # These should be zero indexed
+ record.x1_inside = int(record.x1_inside - 1)
+ record.y1_inside = int(record.y1_inside - 1)
+
+ sensor.record_start_index = sensor.record_start_index - int(1)
+
+ # start time loop
+ for t_index in tqdm(np.arange(index_start, index_end, index_step, dtype=int)):
+
+ # compute the gradients of the stress tensor
+ # these variables do not necessaily need to be stored, they could be computed as needed
+ dsxxdx = np.real(scipy.fft.ifftn(ddx_k_shift_pos * scipy.fft.fftn(sxx_split_x + sxx_split_y, axes=(0,) ), axes=(0,) ))
+ dsyydy = np.real(scipy.fft.ifftn(ddy_k_shift_pos * scipy.fft.fftn(syy_split_x + syy_split_y, axes=(1,) ), axes=(1,) ))
+ temp = sxy_split_x + sxy_split_y
+ dsxydx = np.real(scipy.fft.ifftn(ddx_k_shift_neg * scipy.fft.fftn(temp, axes=(0,) ), axes=(0,) ))
+ dsxydy = np.real(scipy.fft.ifftn(ddy_k_shift_neg * scipy.fft.fftn(temp, axes=(1,) ), axes=(1,) ))
+
+ # calculate the split-field components of ux_sgx and uy_sgy at the next
+ # time step using the components of the stress at the current time step
+ temp = mpml_y * pml_x_sgx
+ ux_split_x = temp * (temp * ux_split_x + dt * rho0_sgx_inv * dsxxdx)
+
+ temp = mpml_x_sgx * pml_y
+ ux_split_y = temp * (temp * ux_split_y + dt * rho0_sgx_inv * dsxydy)
+
+ temp = mpml_y_sgy * pml_x
+ uy_split_x = temp * (temp * uy_split_x + dt * rho0_sgy_inv * dsxydx)
+
+ temp = mpml_x * pml_y_sgy
+ uy_split_y = temp * (temp * uy_split_y + dt * rho0_sgy_inv * dsyydy)
+
+ # add in the pre-scaled velocity source terms
+ if (k_sim.source_ux > t_index):
+ if (source.u_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ ux_split_x[np.unravel_index(k_sim.u_source_pos_index, ux_split_x.shape, order='F')] = k_sim.source.ux[k_sim.u_source_sig_index, t_index]
+ else:
+ # add the source values to the existing field values
+ ux_split_x[np.unravel_index(k_sim.u_source_pos_index, ux_split_x.shape, order='F')] += k_sim.source.ux[k_sim.u_source_sig_index, t_index]
+
+ if (k_sim.source_uy > t_index):
+ if (source.u_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ uy_split_y[np.unravel_index(k_sim.u_source_pos_index, uy_split_y.shape, order='F')] = k_sim.source.uy[k_sim.u_source_sig_index, t_index]
+ else:
+ # add the source values to the existing field values
+ uy_split_y[np.unravel_index(k_sim.u_source_pos_index, uy_split_y.shape, order='F')] += k_sim.source.uy[k_sim.u_source_sig_index, t_index]
+
+
+ # combine split field components
+ # these variables do not necessarily need to be stored, they could be computed when needed
+ ux_sgx = ux_split_x + ux_split_y
+ uy_sgy = uy_split_x + uy_split_y
+
+ # calculate the velocity gradients
+ # these variables do not necessarily need to be stored, they could be computed when needed
+ duxdx = np.real(scipy.fft.ifftn(ddx_k_shift_neg * scipy.fft.fftn(ux_sgx, axes=(0,) ), axes=(0,) ))
+ duxdy = np.real(scipy.fft.ifftn(ddy_k_shift_pos * scipy.fft.fftn(ux_sgx, axes=(1,) ), axes=(1,) ))
+ duydx = np.real(scipy.fft.ifftn(ddx_k_shift_pos * scipy.fft.fftn(uy_sgy, axes=(0,) ), axes=(0,) ))
+ duydy = np.real(scipy.fft.ifftn(ddy_k_shift_neg * scipy.fft.fftn(uy_sgy, axes=(1,) ), axes=(1,) ))
+
+ # update the normal components and shear components of stress tensor using a split field pml
+ if options.kelvin_voigt_model:
+
+ # compute additional gradient terms needed for the Kelvin-Voigt model
+
+ temp = (dsxxdx + dsxydy) * rho0_sgx_inv
+ dduxdxdt = np.real(scipy.fft.ifftn(ddx_k_shift_neg * scipy.fft.fftn(temp, axes=(0,) ), axes=(0,) ))
+ dduxdydt = np.real(scipy.fft.ifftn(ddy_k_shift_pos * scipy.fft.fftn(temp, axes=(1,) ), axes=(1,) ))
+
+ temp = (dsyydy + dsxydx) * rho0_sgy_inv
+ dduydxdt = np.real(scipy.fft.ifftn(ddx_k_shift_pos * scipy.fft.fftn(temp, axes=(0,) ), axes=(0,) ))
+ dduydydt = np.real(scipy.fft.ifftn(ddy_k_shift_neg * scipy.fft.fftn(temp, axes=(1,) ), axes=(1,) ))
+
+ temp = mpml_y * pml_x
+ temp1 = dt * (lame_lambda * duxdx + chi * dduxdxdt)
+ temp2 = two * dt * (mu * duxdx + eta * dduxdxdt)
+
+ sxx_split_x = temp * (temp * sxx_split_x + temp1 + temp2)
+ syy_split_x = temp * (temp * syy_split_x + temp1)
+
+
+ temp = mpml_x * pml_y
+ temp1 = dt * (lame_lambda * duydy + chi * dduydydt)
+ temp2 = two * dt * (mu * duydy + eta * dduydydt)
+
+ sxx_split_y = temp * (temp * sxx_split_y + temp1)
+ syy_split_y = temp * (temp * syy_split_y + temp1 + temp2)
+
+
+ temp = mpml_y_sgy * pml_x_sgx
+ sxy_split_x = temp * (temp * sxy_split_x + dt * (mu_sgxy * duydx + eta_sgxy * dduydxdt))
+
+ temp = mpml_x_sgx * pml_y_sgy
+ sxy_split_y = temp * (temp * sxy_split_y + dt * (mu_sgxy * duxdy + eta_sgxy * dduxdydt))
+
+ else:
+ # update the normal and shear components of the stress tensor using
+ # a lossless elastic model with a split-field multi-axial pml
+
+ temp = mpml_y * pml_x
+ temp1 = dt * lame_lambda * duxdx
+ temp2 = dt * two * mu * duxdx
+
+ sxx_split_x = temp * (temp * sxx_split_x + temp1 + temp2)
+ syy_split_x = temp * (temp * syy_split_x + temp1)
+
+
+ temp = mpml_x * pml_y
+ temp1 = dt * lame_lambda * duydy
+ temp2 = dt * two * mu * duydy
+
+ sxx_split_y = temp * (temp * sxx_split_y + temp1)
+ syy_split_y = temp * (temp * syy_split_y + temp1 + temp2)
+
+
+ temp = mpml_y_sgy * pml_x_sgx
+ sxy_split_x = temp * (temp * sxy_split_x + dt * mu_sgxy * duydx)
+
+
+ temp = mpml_x_sgx * pml_y_sgy
+ sxy_split_y = temp * (temp * sxy_split_y + dt * mu_sgxy * duxdy)
+
+
+ # add stress source terms
+ if (k_sim.source_sxx is not False and t_index < np.shape(source.sxx)[1]):
+ if (source.s_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ sxx_split_x[np.unravel_index(k_sim.s_source_pos_index, sxx_split_x.shape, order='F')] = k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+ sxx_split_y[np.unravel_index(k_sim.s_source_pos_index, sxx_split_y.shape, order='F')] = k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+ else:
+ # spatially and temporally varying source
+ sxx_split_x[np.unravel_index(k_sim.s_source_pos_index, sxx_split_x.shape, order='F')] += k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+ sxx_split_y[np.unravel_index(k_sim.s_source_pos_index, sxx_split_y.shape, order='F')] += k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+
+ if (k_sim.source_syy is not False and t_index < np.shape(source.syy)[1]):
+ if (source.s_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ syy_split_x[np.unravel_index(k_sim.s_source_pos_index, syy_split_x.shape, order='F')] = k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+ syy_split_y[np.unravel_index(k_sim.s_source_pos_index, syy_split_y.shape, order='F')] = k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+ else:
+ # spatially and temporally varying source
+ syy_split_x[np.unravel_index(k_sim.s_source_pos_index, syy_split_x.shape, order='F')] += k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+ syy_split_y[np.unravel_index(k_sim.s_source_pos_index, syy_split_y.shape, order='F')] += k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+
+ if (k_sim.source_sxy is not False and t_index < k_sim.source_sxy):
+ if (source.s_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ sxy_split_x[np.unravel_index(k_sim.s_source_pos_index, sxy_split_x.shape, order='F')] = k_sim.source.sxy[k_sim.s_source_sig_index, t_index]
+ sxy_split_y[np.unravel_index(k_sim.s_source_pos_index, sxy_split_y.shape, order='F')] = k_sim.source.sxy[k_sim.s_source_sig_index, t_index]
+ else:
+ # spatially and temporally varying source
+ sxy_split_x[np.unravel_index(k_sim.s_source_pos_index, sxy_split_x.shape, order='F')] += k_sim.source.sxy[k_sim.s_source_sig_index, t_index]
+ sxy_split_y[np.unravel_index(k_sim.s_source_pos_index, sxy_split_y.shape, order='F')] += k_sim.source.sxy[k_sim.s_source_sig_index, t_index]
+
+
+ # compute pressure from normal components of the stress
+ p = -(sxx_split_x + sxx_split_y + syy_split_x + syy_split_y) / two
+
+ # extract required sensor data from the pressure and particle velocity
+ # fields if the number of time steps elapsed is greater than
+ # sensor.record_start_index (defaults to 1)
+ if ((k_sim.use_sensor is not False) and (not k_sim.elastic_time_rev) and (t_index >= sensor.record_start_index)):
+
+ # update index for data storage
+ file_index: int = t_index - sensor.record_start_index
+
+ # run sub-function to extract the required data
+ extract_options = dotdict({'record_u_non_staggered': k_sim.record.u_non_staggered,
+ 'record_u_split_field': k_sim.record.u_split_field,
+ 'record_I': k_sim.record.I,
+ 'record_I_avg': k_sim.record.I_avg,
+ 'binary_sensor_mask': k_sim.binary_sensor_mask,
+ 'record_p': k_sim.record.p,
+ 'record_p_max': k_sim.record.p_max,
+ 'record_p_min': k_sim.record.p_min,
+ 'record_p_rms': k_sim.record.p_rms,
+ 'record_p_max_all': k_sim.record.p_max_all,
+ 'record_p_min_all': k_sim.record.p_min_all,
+ 'record_u': k_sim.record.u,
+ 'record_u_max': k_sim.record.u_max,
+ 'record_u_min': k_sim.record.u_min,
+ 'record_u_rms': k_sim.record.u_rms,
+ 'record_u_max_all': k_sim.record.u_max_all,
+ 'record_u_min_all': k_sim.record.u_min_all,
+ 'compute_directivity': False})
+
+ sensor_data = extract_sensor_data(2, sensor_data, file_index, k_sim.sensor_mask_index,
+ extract_options, k_sim.record, p, ux_sgx, uy_sgy)
+
+ # update variable used for timing variable to exclude the first
+ # time step if plotting is enabled
+ if t_index == 0:
+ clock1 = TicToc()
+ clock1.tic()
+
+
+ # update command line status
+ t1 = timer.toc()
+ t1_scale = scale_time(t1)
+ print('\tsimulation completed in', t1_scale)
+
+ # =========================================================================
+ # CLEAN UP
+ # =========================================================================
+
+ # # clean up used figures
+ # if options.plot_sim:
+ # # close(img);
+ # # close(pbar);
+ # pass
+
+ # # save the movie frames to disk
+ # if options.record_movie:
+ # # close(video_obj);
+ # pass
+
+ # save the final acoustic pressure if required
+ if (k_sim.record.p_final or k_sim.elastic_time_rev):
+ sensor_data.p_final = p[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside]
+
+ # save the final particle velocity if required
+ if k_sim.record.u_final:
+ sensor_data.ux_final = ux_sgx[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside]
+ sensor_data.uy_final = uy_sgy[record.x1_inside:record.x2_inside, record.y1_inside:record.y2_inside]
+
+ # # run subscript to cast variables back to double precision if required
+ # if options.data_recast:
+ # #kspaceFirstOrder_dataRecast;
+ # pass
+
+ # run subscript to compute and save intensity values
+ if (k_sim.use_sensor is not False and (not k_sim.elastic_time_rev) and (k_sim.record.I or k_sim.record.I_avg)):
+ # save_intensity_matlab_code = True
+ # kspaceFirstOrder_saveIntensity;
+ pass
+
+ # reorder the sensor points if a binary sensor mask was used for Cartesian
+ # sensor mask nearest neighbour interpolation (this is performed after
+ # recasting as the GPU toolboxes do not all support this subscript)
+ if (k_sim.use_sensor is not False and k_sim.reorder_data):
+ # kspaceFirstOrder_reorderCartData;
+ pass
+
+ # filter the recorded time domain pressure signals if transducer filter
+ # parameters are given
+ if (k_sim.use_sensor is not False and not k_sim.elastic_time_rev and hasattr(sensor, 'frequency_response') and
+ sensor.frequency_response is not None):
+ fs = 1.0 / kgrid.dt
+ sensor_data.p = gaussian_filter(sensor_data.p, fs, sensor.frequency_response[0], sensor.frequency_response[1])
+
+ # reorder the sensor points if cuboid corners is used (outputs are indexed
+ # as [X, Y, T] or [X, Y] rather than [sensor_index, time_index]
+ if options.cuboid_corners:
+ sensor_data = reorder_sensor_data(kgrid, sensor, sensor_data)
+
+ if k_sim.elastic_time_rev:
+ # if computing time reversal, reassign sensor_data.p_final to sensor_data
+ sensor_data = sensor_data.p_final
+ elif (k_sim.use_sensor is False):
+ # if sensor is not used, return empty sensor data
+ sensor_data = None
+ elif ((not hasattr(sensor, 'record')) and (not options.cuboid_corners)):
+ # if sensor.record is not given by the user, reassign sensor_data.p to sensor_data
+ sensor_data = sensor_data.p
+
+ # update command line status
+ t_total = t0 + t1
+ print('\ttotal computation time', scale_time(t_total), '\n')
+
+ return sensor_data
diff --git a/kwave/pstdElastic3D.py b/kwave/pstdElastic3D.py
new file mode 100644
index 000000000..968dd78a5
--- /dev/null
+++ b/kwave/pstdElastic3D.py
@@ -0,0 +1,1460 @@
+import numpy as np
+from scipy.interpolate import interpn
+import scipy.fft
+from tqdm import tqdm
+from typing import Union
+from copy import deepcopy
+
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksensor import kSensor
+from kwave.ksource import kSource
+from kwave.kWaveSimulation import kWaveSimulation
+from kwave.kWaveSimulation_helper import extract_sensor_data, save_intensity, reorder_cuboid_corners
+
+from kwave.options.simulation_options import SimulationOptions
+
+from kwave.ktransducer import NotATransducer
+
+from kwave.utils.conversion import db2neper
+from kwave.utils.data import scale_time
+from kwave.utils.filters import gaussian_filter
+from kwave.utils.pml import get_pml
+from kwave.utils.signals import reorder_sensor_data
+from kwave.utils.tictoc import TicToc
+from kwave.utils.dotdictionary import dotdict
+
+
+def pstd_elastic_3d(kgrid: kWaveGrid,
+ source: kSource,
+ sensor: Union[NotATransducer, kSensor],
+ medium: kWaveMedium,
+ simulation_options: SimulationOptions):
+ """
+ 3D time-domain simulation of elastic wave propagation.
+
+ DESCRIPTION:
+
+ pstd_elastic_3d simulates the time-domain propagation of elastic waves
+ through a three-dimensional homogeneous or heterogeneous medium given
+ four input structures: kgrid, medium, source, and sensor. The
+ computation is based on a pseudospectral time domain model which
+ accounts for viscoelastic absorption and heterogeneous material
+ parameters. At each time-step (defined by dt and kgrid.Nt or
+ kgrid.t_array), the wavefield parameters at the positions defined by
+ sensor.mask are recorded and stored. If kgrid.t_array is set to
+ 'auto', this array is automatically generated using the makeTime
+ method of the kWaveGrid class. An anisotropic absorbing boundary
+ layer called a perfectly matched layer (PML) is implemented to
+ prevent waves that leave one side of the domain being reintroduced
+ from the opposite side (a consequence of using the FFT to compute the
+ spatial derivatives in the wave equation). This allows infinite
+ domain simulations to be computed using small computational grids.
+
+ An initial pressure distribution can be specified by assigning a
+ matrix of pressure values the same size as the computational grid to
+ source.p0. This is then assigned to the normal components of the
+ stress within the simulation function. A time varying stress source
+ can similarly be specified by assigning a binary matrix (i.e., a
+ matrix of 1's and 0's with the same dimensions as the computational
+ grid) to source.s_mask where the 1's represent the grid points that
+ form part of the source. The time varying input signals are then
+ assigned to source.sxx, source.syy, source.szz, source.sxy,
+ source.sxz, and source.syz. These can be a single time series (in
+ which case it is applied to all source elements), or a matrix of time
+ series following the source elements using MATLAB's standard
+ column-wise linear matrix index ordering. A time varying velocity
+ source can be specified in an analogous fashion, where the source
+ location is specified by source.u_mask, and the time varying input
+ velocity is assigned to source.ux, source.uy, and source.uz.
+
+ The field values are returned as arrays of time series at the sensor
+ locations defined by sensor.mask. This can be defined in three
+ different ways. (1) As a binary matrix (i.e., a matrix of 1's and 0's
+ with the same dimensions as the computational grid) representing the
+ grid points within the computational grid that will collect the data.
+ (2) As the grid coordinates of two opposing corners of a cuboid in
+ the form [x1 y1 z1 x2 y2 z2]. This is equivalent to using a
+ binary sensor mask covering the same region, however, the output is
+ indexed differently as discussed below. (3) As a series of Cartesian
+ coordinates within the grid which specify the location of the
+ pressure values stored at each time step. If the Cartesian
+ coordinates don't exactly match the coordinates of a grid point, the
+ output values are calculated via interpolation. The Cartesian points
+ must be given as a 3 by N matrix corresponding to the x, y, and z
+ positions, respectively, where the Cartesian origin is assumed to be
+ in the center of the grid. If no output is required, the sensor input
+ can be replaced with `None`.
+
+ If sensor.mask is given as a set of Cartesian coordinates, the
+ computed sensor_data is returned in the same order. If sensor.mask is
+ given as a binary matrix, sensor_data is returned using MATLAB's
+ standard column-wise linear matrix index ordering. In both cases, the
+ recorded data is indexed as sensor_data(sensor_point_index,
+ time_index). For a binary sensor mask, the field values at a
+ particular time can be restored to the sensor positions within the
+ computation grid using unmaskSensorData. If sensor.mask is given as a
+ list of cuboid corners, the recorded data is indexed as
+ sensor_data(cuboid_index).p(x_index, y_index, z_index, time_index),
+ where x_index, y_index, and z_index correspond to the grid index
+ within the cuboid, and cuboid_index corresponds to the number of the
+ cuboid if more than one is specified.
+
+ By default, the recorded acoustic pressure field is passed directly
+ to the output sensor_data. However, other acoustic parameters can
+ also be recorded by setting sensor.record to a cell array of the form
+ {'p', 'u', 'p_max', }. For example, both the particle velocity and
+ the acoustic pressure can be returned by setting sensor.record =
+ {'p', 'u'}. If sensor.record is given, the output sensor_data is
+ returned as a structure with the different outputs appended as
+ structure fields. For example, if sensor.record = {'p', 'p_final',
+ 'p_max', 'u'}, the output would contain fields sensor_data.p,
+ sensor_data.p_final, sensor_data.p_max, sensor_data.ux,
+ sensor_data.uy, and sensor_data.uz. Most of the output parameters are
+ recorded at the given sensor positions and are indexed as
+ sensor_data.field(sensor_point_index, time_index) or
+ sensor_data(cuboid_index).field(x_index, y_index, z_index,
+ time_index) if using a sensor mask defined as cuboid corners. The
+ exceptions are the averaged quantities ('p_max', 'p_rms', 'u_max',
+ 'p_rms', 'I_avg'), the 'all' quantities ('p_max_all', 'p_min_all',
+ 'u_max_all', 'u_min_all'), and the final quantities ('p_final',
+ 'u_final'). The averaged quantities are indexed as
+ sensor_data.p_max(sensor_point_index) or
+ sensor_data(cuboid_index).p_max(x_index, y_index, z_index) if using
+ cuboid corners, while the final and 'all' quantities are returned
+ over the entire grid and are always indexed as
+ sensor_data.p_final(nx, ny, nz), regardless of the type of sensor
+ mask.
+
+ pstd_elastic_3d may also be used for time reversal image reconstruction
+ by assigning the time varying pressure recorded over an arbitrary
+ sensor surface to the input field sensor.time_reversal_boundary_data.
+ This data is then enforced in time reversed order as a time varying
+ Dirichlet boundary condition over the sensor surface given by
+ sensor.mask. The boundary data must be indexed as
+ sensor.time_reversal_boundary_data(sensor_point_index, time_index).
+ If sensor.mask is given as a set of Cartesian coordinates, the
+ boundary data must be given in the same order. An equivalent binary
+ sensor mask (computed using nearest neighbour interpolation) is then
+ used to place the pressure values into the computational grid at each
+ time step. If sensor.mask is given as a binary matrix of sensor
+ points, the boundary data must be ordered using matlab's standard
+ column-wise linear matrix indexing - this means, Fortran ordering.
+
+ USAGE:
+ sensor_data = pstd_elastic_3d(kgrid, medium, source, sensor, options)
+
+ INPUTS:
+ The minimum fields that must be assigned to run an initial value problem
+ (for example, a photoacoustic forward simulation) are marked with a *.
+
+ kgrid* - k-Wave grid object returned by kWaveGrid
+ containing Cartesian and k-space grid fields
+ kgrid.t_array * - evenly spaced array of time values [s] (set
+ to 'auto' by kWaveGrid)
+
+ medium.sound_speed_compression*
+ - compressional sound speed distribution
+ within the acoustic medium [m/s]
+ medium.sound_speed_shear*
+ - shear sound speed distribution within the
+ acoustic medium [m/s]
+ medium.density * - density distribution within the acoustic
+ medium [kg/m^3]
+ medium.alpha_coeff_compression
+ - absorption coefficient for compressional
+ waves [dB/(MHz^2 cm)]
+ medium.alpha_coeff_shear
+ - absorption coefficient for shear waves
+ [dB/(MHz^2 cm)]
+
+ source.p0* - initial pressure within the acoustic medium
+ source.sxx - time varying stress at each of the source
+ positions given by source.s_mask
+ source.syy - time varying stress at each of the source
+ positions given by source.s_mask
+ source.szz - time varying stress at each of the source
+ positions given by source.s_mask
+ source.sxy - time varying stress at each of the source
+ positions given by source.s_mask
+ source.sxz - time varying stress at each of the source
+ positions given by source.s_mask
+ source.syz - time varying stress at each of the source
+ positions given by source.s_mask
+ source.s_mask - binary matrix specifying the positions of
+ the time varying stress source distributions
+ source.s_mode - optional input to control whether the input
+ stress is injected as a mass source or
+ enforced as a dirichlet boundary condition
+ valid inputs are 'additive' (the default) or
+ 'dirichlet'
+ source.ux - time varying particle velocity in the
+ x-direction at each of the source positions
+ given by source.u_mask
+ source.uy - time varying particle velocity in the
+ y-direction at each of the source positions
+ given by source.u_mask
+ source.uz - time varying particle velocity in the
+ z-direction at each of the source positions
+ given by source.u_mask
+ source.u_mask - binary matrix specifying the positions of
+ the time varying particle velocity
+ distribution
+ source.u_mode - optional input to control whether the input
+ velocity is applied as a force source or
+ enforced as a dirichlet boundary condition
+ valid inputs are 'additive' (the default) or
+ 'dirichlet'
+
+ sensor.mask* - binary matrix or a set of Cartesian points
+ where the pressure is recorded at each
+ time-step
+ sensor.record - cell array of the acoustic parameters to
+ record in the form sensor.record = ['p',
+ 'u'] valid inputs are:
+
+ 'p' (acoustic pressure)
+ 'p_max' (maximum pressure)
+ 'p_min' (minimum pressure)
+ 'p_rms' (RMS pressure)
+ 'p_final' (final pressure field at all grid points)
+ 'p_max_all' (maximum pressure at all grid points)
+ 'p_min_all' (minimum pressure at all grid points)
+ 'u' (particle velocity)
+ 'u_max' (maximum particle velocity)
+ 'u_min' (minimum particle velocity)
+ 'u_rms' (RMS particle velocity)
+ 'u_final' (final particle velocity field at all grid points)
+ 'u_max_all' (maximum particle velocity at all grid points)
+ 'u_min_all' (minimum particle velocity at all grid points)
+ 'u_non_staggered' (particle velocity on non-staggered grid)
+ 'u_split_field' (particle velocity on non-staggered grid split
+ into compressional and shear components)
+ 'I' (time varying acoustic intensity)
+ 'I_avg' (average acoustic intensity)
+
+ NOTE: the acoustic pressure outputs are calculated from the
+ normal stress via: p = -(sxx + syy + szz)/3
+
+ sensor.record_start_index
+ - time index at which the sensor should start
+ recording the data specified by
+ sensor.record (default = 1)
+ sensor.time_reversal_boundary_data
+ - time varying pressure enforced as a
+ Dirichlet boundary condition over
+ sensor.mask
+
+ Note: For a heterogeneous medium, medium.sound_speed_compression,
+ medium.sound_speed_shear, and medium.density must be given in matrix form
+ with the same dimensions as kgrid. For a homogeneous medium, these can be
+ given as scalar values.
+
+ OPTIONAL INPUTS:
+ Optional 'string', value pairs that may be used to modify the default
+ computational settings.
+
+ See .html help file for details.
+
+ OUTPUTS:
+ If sensor.record is not defined by the user:
+ sensor_data - time varying pressure recorded at the sensor
+ positions given by sensor.mask
+
+ If sensor.record is defined by the user:
+ sensor_data.p - time varying pressure recorded at the
+ sensor positions given by sensor.mask
+ (returned if 'p' is set)
+ sensor_data.p_max - maximum pressure recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'p_max' is set)
+ sensor_data.p_min - minimum pressure recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'p_min' is set)
+ sensor_data.p_rms - rms of the time varying pressure recorded
+ at the sensor positions given by
+ sensor.mask (returned if 'p_rms' is set)
+ sensor_data.p_final - final pressure field at all grid points
+ within the domain (returned if 'p_final' is
+ set)
+ sensor_data.p_max_all - maximum pressure recorded at all grid points
+ within the domain (returned if 'p_max_all'
+ is set)
+ sensor_data.p_min_all - minimum pressure recorded at all grid points
+ within the domain (returned if 'p_min_all'
+ is set)
+ sensor_data.ux - time varying particle velocity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'u' is
+ set)
+ sensor_data.uy - time varying particle velocity in the
+ y-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'u' is
+ set)
+ sensor_data.uz - time varying particle velocity in the
+ z-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'u' is
+ set)
+ sensor_data.ux_max - maximum particle velocity in the x-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_max' is set)
+ sensor_data.uy_max - maximum particle velocity in the y-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_max' is set)
+ sensor_data.uz_max - maximum particle velocity in the z-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_max' is set)
+ sensor_data.ux_min - minimum particle velocity in the x-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_min' is set)
+ sensor_data.uy_min - minimum particle velocity in the y-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_min' is set)
+ sensor_data.uz_min - minimum particle velocity in the z-direction
+ recorded at the sensor positions given by
+ sensor.mask (returned if 'u_min' is set)
+ sensor_data.ux_rms - rms of the time varying particle velocity in
+ the x-direction recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_rms' is set)
+ sensor_data.uy_rms - rms of the time varying particle velocity in
+ the y-direction recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_rms' is set)
+ sensor_data.uz_rms - rms of the time varying particle velocity
+ in the z-direction recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_rms' is set)
+ sensor_data.ux_final - final particle velocity field in the
+ x-direction at all grid points within the
+ domain (returned if 'u_final' is set)
+ sensor_data.uy_final - final particle velocity field in the
+ y-direction at all grid points within the
+ domain (returned if 'u_final' is set)
+ sensor_data.uz_final - final particle velocity field in the
+ z-direction at all grid points within the
+ domain (returned if 'u_final' is set)
+ sensor_data.ux_max_all - maximum particle velocity in the x-direction
+ recorded at all grid points within the
+ domain (returned if 'u_max_all' is set)
+ sensor_data.uy_max_all - maximum particle velocity in the y-direction
+ recorded at all grid points within the
+ domain (returned if 'u_max_all' is set)
+ sensor_data.uz_max_all - maximum particle velocity in the z-direction
+ recorded at all grid points within the
+ domain (returned if 'u_max_all' is set)
+ sensor_data.ux_min_all - minimum particle velocity in the x-direction
+ recorded at all grid points within the
+ domain (returned if 'u_min_all' is set)
+ sensor_data.uy_min_all - minimum particle velocity in the y-direction
+ recorded at all grid points within the
+ domain (returned if 'u_min_all' is set)
+ sensor_data.uz_min_all - minimum particle velocity in the z-direction
+ recorded at all grid points within the
+ domain (returned if 'u_min_all' is set)
+ sensor_data.ux_non_staggered
+ - time varying particle velocity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask after shifting to the
+ non-staggered grid (returned if
+ 'u_non_staggered' is set)
+ sensor_data.uy_non_staggered
+ - time varying particle velocity in the
+ y-direction recorded at the sensor positions
+ given by sensor.mask after shifting to the
+ non-staggered grid (returned if
+ 'u_non_staggered' is set)
+ sensor_data.uz_non_staggered
+ - time varying particle velocity in the
+ z-direction recorded at the sensor positions
+ given by sensor.mask after shifting to the
+ non-staggered grid (returned if
+ 'u_non_staggered' is set)
+ sensor_data.ux_split_p - compressional component of the time varying
+ particle velocity in the x-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.ux_split_s - shear component of the time varying particle
+ velocity in the x-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.uy_split_p - compressional component of the time varying
+ particle velocity in the y-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.uy_split_s - shear component of the time varying particle
+ velocity in the y-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.uz_split_p - compressional component of the time varying
+ particle velocity in the z-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.uz_split_s - shear component of the time varying particle
+ velocity in the z-direction on the
+ non-staggered grid recorded at the sensor
+ positions given by sensor.mask (returned if
+ 'u_split_field' is set)
+ sensor_data.Ix - time varying acoustic intensity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I' is
+ set)
+ sensor_data.Iy - time varying acoustic intensity in the
+ y-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I' is
+ set)
+ sensor_data.Iz - time varying acoustic intensity in the
+ z-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I' is
+ set)
+ sensor_data.Ix_avg - average acoustic intensity in the
+ x-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I_avg' is
+ set)
+ sensor_data.Iy_avg - average acoustic intensity in the
+ y-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I_avg' is
+ set)
+ sensor_data.Iz_avg - average acoustic intensity in the
+ z-direction recorded at the sensor positions
+ given by sensor.mask (returned if 'I_avg' is
+ set)
+
+ ABOUT:
+ author - Bradley Treeby & Ben Cox
+ date - 11th March 2013
+ last update - 13th January 2019
+
+ This function is part of the k-Wave Toolbox (http://www.k-wave.org)
+ Copyright (C) 2013-2019 Bradley Treeby and Ben Cox
+
+ See also kspaceFirstOrder3D, kWaveGrid, pstdElastic2D
+
+ This file is part of k-Wave. k-Wave is free software: you can
+ redistribute it and/or modify it under the terms of the GNU Lesser
+ General Public License as published by the Free Software Foundation,
+ either version 3 of the License, or (at your option) any later version.
+
+ k-Wave is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with k-Wave. If not, see .
+ """
+
+# =========================================================================
+# CHECK INPUT STRUCTURES AND OPTIONAL INPUTS
+# =========================================================================
+
+ # fortran ordered
+ myOrder = 'F'
+
+ # start the timer and store the start time
+ timer = TicToc()
+ timer.tic()
+
+ # build simulation object with flags and formatted data containers
+ k_sim = kWaveSimulation(kgrid=kgrid,
+ source=source,
+ sensor=sensor,
+ medium=medium,
+ simulation_options=simulation_options)
+
+ # run helper script to check inputs
+ k_sim.input_checking('pstd_elastic_3d')
+
+ # TODO - if cuboid corners with more than one choice, then is a list
+ sensor_data = k_sim.sensor_data
+
+ options = k_sim.options
+
+ rho0 = np.atleast_1d(k_sim.rho0) # maybe at least 3d?
+
+ m_rho0 : int = np.squeeze(rho0).ndim
+
+ # assign the lame parameters
+ mu = medium.sound_speed_shear**2 * medium.density
+ lame_lambda = medium.sound_speed_compression**2 * medium.density - 2.0 * mu
+ m_mu : int = np.squeeze(mu).ndim
+
+ # assign the viscosity coefficients
+ if (options.kelvin_voigt_model):
+ # print(medium.alpha_coeff_shear, medium.alpha_coeff_compression, options.kelvin_voigt_model)
+ eta = 2.0 * rho0 * medium.sound_speed_shear**3 * db2neper(medium.alpha_coeff_shear, 2)
+ chi = 2.0 * rho0 * medium.sound_speed_compression**3 * db2neper(medium.alpha_coeff_compression, 2) - 2.0 * eta
+ m_eta : int = np.squeeze(eta).ndim
+
+ # =========================================================================
+ # CALCULATE MEDIUM PROPERTIES ON STAGGERED GRID
+ # =========================================================================
+
+ # calculate the values of the density at the staggered grid points
+ # using the arithmetic average [1, 2], where sgx = (x + dx/2, y),
+ # sgy = (x, y + dy/2) and sgz = (x, y, z + dz/2)
+ if (m_rho0 == 3 and (options.use_sg)):
+ # rho0 is heterogeneous and staggered grids are used
+
+ points = (np.squeeze(k_sim.kgrid.x_vec), np.squeeze(k_sim.kgrid.y_vec), np.squeeze(k_sim.kgrid.z_vec))
+
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec) + k_sim.kgrid.dx / 2.0,
+ np.squeeze(k_sim.kgrid.y_vec),
+ np.squeeze(k_sim.kgrid.z_vec),
+ indexing='ij',)
+ interp_points = np.moveaxis(mg, 0, -1)
+ rho0_sgx = interpn(points, k_sim.rho0, interp_points, method='linear', bounds_error=False)
+
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec),
+ np.squeeze(k_sim.kgrid.y_vec) + k_sim.kgrid.dy / 2.0,
+ np.squeeze(k_sim.kgrid.z_vec),
+ indexing='ij')
+ interp_points = np.moveaxis(mg, 0, -1)
+ rho0_sgy = interpn(points, k_sim.rho0, interp_points, method='linear', bounds_error=False)
+
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec),
+ np.squeeze(k_sim.kgrid.y_vec),
+ np.squeeze(k_sim.kgrid.z_vec) + k_sim.kgrid.dz / 2.0,
+ indexing='ij')
+ interp_points = np.moveaxis(mg, 0, -1)
+ rho0_sgz = interpn(points, k_sim.rho0, interp_points, method='linear', bounds_error=False)
+
+ # set values outside of the interpolation range to original values
+ rho0_sgx[np.isnan(rho0_sgx)] = rho0[np.isnan(rho0_sgx)]
+ rho0_sgy[np.isnan(rho0_sgy)] = rho0[np.isnan(rho0_sgy)]
+ rho0_sgz[np.isnan(rho0_sgz)] = rho0[np.isnan(rho0_sgz)]
+ else:
+ # rho0 is homogeneous or staggered grids are not used
+ rho0_sgx = rho0
+ rho0_sgy = rho0
+ rho0_sgz = rho0
+
+ # elementwise reciprocal of rho0 so it doesn't have to be done each time step
+ rho0_sgx_inv = 1.0 / rho0_sgx
+ rho0_sgy_inv = 1.0 / rho0_sgy
+ rho0_sgz_inv = 1.0 / rho0_sgz
+
+ # clear unused variables if not using them in _saveToDisk
+ if not options.save_to_disk:
+ del rho0_sgx
+ del rho0_sgy
+ del rho0_sgz
+
+ # calculate the values of mu at the staggered grid points using the
+ # harmonic average [1, 2], where sgxy = (x + dx/2, y + dy/2, z), etc
+ if (m_mu == 3 and options.use_sg):
+ # interpolation points
+ points = (np.squeeze(k_sim.kgrid.x_vec), np.squeeze(k_sim.kgrid.y_vec), np.squeeze(k_sim.kgrid.z_vec))
+
+ # mu is heterogeneous and staggered grids are used
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec) + k_sim.kgrid.dx / 2.0,
+ np.squeeze(k_sim.kgrid.y_vec) + k_sim.kgrid.dy / 2.0,
+ np.squeeze(k_sim.kgrid.z_vec),
+ indexing='ij')
+ interp_points = np.moveaxis(mg, 0, -1)
+ with np.errstate(divide='ignore', invalid='ignore'):
+ mu_sgxy = 1.0 / interpn(points, 1.0 / mu, interp_points, method='linear', bounds_error=False)
+
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec) + k_sim.kgrid.dx / 2.0,
+ np.squeeze(k_sim.kgrid.y_vec),
+ np.squeeze(k_sim.kgrid.z_vec) + k_sim.kgrid.dz / 2.0,
+ indexing='ij')
+ interp_points = np.moveaxis(mg, 0, -1)
+ with np.errstate(divide='ignore', invalid='ignore'):
+ mu_sgxz = 1.0 / interpn(points, 1.0 / mu, interp_points, method='linear', bounds_error=False)
+
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec),
+ np.squeeze(k_sim.kgrid.y_vec) + k_sim.kgrid.dy / 2.0,
+ np.squeeze(k_sim.kgrid.z_vec) + k_sim.kgrid.dz / 2.0,
+ indexing='ij')
+ interp_points = np.moveaxis(mg, 0, -1)
+ with np.errstate(divide='ignore', invalid='ignore'):
+ mu_sgyz = 1.0 / interpn(points, 1.0 / mu, interp_points, method='linear', bounds_error=False)
+
+ # set values outside of the interpolation range to original values
+ mu_sgxy[np.isnan(mu_sgxy)] = mu[np.isnan(mu_sgxy)]
+ mu_sgxz[np.isnan(mu_sgxz)] = mu[np.isnan(mu_sgxz)]
+ mu_sgyz[np.isnan(mu_sgyz)] = mu[np.isnan(mu_sgyz)]
+
+ else:
+ # mu is homogeneous or staggered grids are not used
+ mu_sgxy = mu
+ mu_sgxz = mu
+ mu_sgyz = mu
+
+
+ # calculate the values of eta at the staggered grid points using the
+ # harmonic average [1, 2], where sgxy = (x + dx/2, y + dy/2, z) etc
+ if options.kelvin_voigt_model:
+ if m_eta == 3 and options.use_sg:
+
+ points = (np.squeeze(k_sim.kgrid.x_vec), np.squeeze(k_sim.kgrid.y_vec), np.squeeze(k_sim.kgrid.z_vec))
+
+ # eta is heterogeneous and staggered grids are used
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec) + k_sim.kgrid.dx / 2.0,
+ np.squeeze(k_sim.kgrid.y_vec) + k_sim.kgrid.dy / 2.0,
+ np.squeeze(k_sim.kgrid.z_vec),
+ indexing='ij')
+ interp_points = np.moveaxis(mg, 0, -1)
+ with np.errstate(divide='ignore', invalid='ignore'):
+ eta_sgxy = 1.0 / interpn(points, 1.0 / eta, interp_points, method='linear', bounds_error=False)
+
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec) + k_sim.kgrid.dx / 2.0,
+ np.squeeze(k_sim.kgrid.y_vec),
+ np.squeeze(k_sim.kgrid.z_vec) + k_sim.kgrid.dz / 2.0,
+ indexing='ij')
+ interp_points = np.moveaxis(mg, 0, -1)
+ with np.errstate(divide='ignore', invalid='ignore'):
+ eta_sgxz = 1.0 / interpn(points, 1.0 / eta, interp_points, method='linear', bounds_error=False)
+
+ mg = np.meshgrid(np.squeeze(k_sim.kgrid.x_vec),
+ np.squeeze(k_sim.kgrid.y_vec) + k_sim.kgrid.dy / 2.0,
+ np.squeeze(k_sim.kgrid.z_vec) + k_sim.kgrid.dz / 2.0,
+ indexing='ij')
+ interp_points = np.moveaxis(mg, 0, -1)
+ with np.errstate(divide='ignore', invalid='ignore'):
+ eta_sgyz = 1.0 / interpn(points, 1.0 / eta, interp_points, method='linear', bounds_error=False)
+
+ # set values outside of the interpolation range to original values
+ eta_sgxy[np.isnan(eta_sgxy)] = eta[np.isnan(eta_sgxy)]
+ eta_sgxz[np.isnan(eta_sgxz)] = eta[np.isnan(eta_sgxz)]
+ eta_sgyz[np.isnan(eta_sgyz)] = eta[np.isnan(eta_sgyz)]
+
+ else:
+
+ # eta is homogeneous or staggered grids are not used
+ eta_sgxy = eta
+ eta_sgxz = eta
+ eta_sgyz = eta
+
+
+
+ # [1] Moczo, P., Kristek, J., Vavry?uk, V., Archuleta, R. J., & Halada, L.
+ # (2002). 3D heterogeneous staggered-grid finite-difference modeling of
+ # seismic motion with volume harmonic and arithmetic averaging of elastic
+ # moduli and densities. Bulletin of the Seismological Society of America,
+ # 92(8), 3042-3066.
+
+ # [2] Toyoda, M., Takahashi, D., & Kawai, Y. (2012). Averaged material
+ # parameters and boundary conditions for the vibroacoustic
+ # finite-difference time-domain method with a nonuniform mesh. Acoustical
+ # Science and Technology, 33(4), 273-276.
+
+ # =========================================================================
+ # RECORDER
+ # =========================================================================
+
+ record = k_sim.record
+
+
+ # =========================================================================
+ # PREPARE DERIVATIVE AND PML OPERATORS
+ # =========================================================================
+
+ # get the regular PML operators based on the reference sound speed and PML settings
+ Nx, Ny, Nz = k_sim.kgrid.Nx, k_sim.kgrid.Ny, k_sim.kgrid.Nz
+ dx, dy, dz = k_sim.kgrid.dx, k_sim.kgrid.dy, k_sim.kgrid.dz
+ dt = k_sim.dt
+ Nt = k_sim.kgrid.Nt
+
+ pml_x_alpha, pml_y_alpha, pml_z_alpha = options.pml_x_alpha, options.pml_y_alpha, options.pml_z_alpha
+ pml_x_size, pml_y_size, pml_z_size = options.pml_x_size, options.pml_y_size, options.pml_z_size
+
+ multi_axial_pml_ratio = options.multi_axial_PML_ratio
+
+ c_ref = k_sim.c_ref
+
+ # print("pml alphas:", pml_x_alpha, pml_y_alpha, pml_z_alpha)
+ # print("pml_sizes:", pml_x_size, pml_y_size, pml_z_size)
+
+ # get the regular PML operators based on the reference sound speed and PML settings
+ pml_x = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, False, 0)
+ pml_x_sgx = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, options.use_sg, 0)
+ pml_y = get_pml(Ny, dy, dt, c_ref, pml_y_size, pml_y_alpha, False, 1)
+ pml_y_sgy = get_pml(Ny, dy, dt, c_ref, pml_y_size, pml_y_alpha, options.use_sg, 1)
+ pml_z = get_pml(Nz, dz, dt, c_ref, pml_z_size, pml_z_alpha, False, 2)
+ pml_z_sgz = get_pml(Nz, dz, dt, c_ref, pml_z_size, pml_z_alpha, options.use_sg, 2)
+
+ # get the multi-axial PML operators
+ mpml_x = get_pml(Nx, dx, dt, c_ref, pml_x_size, multi_axial_pml_ratio * pml_x_alpha, False, 0)
+ mpml_x_sgx = get_pml(Nx, dx, dt, c_ref, pml_x_size, multi_axial_pml_ratio * pml_x_alpha, options.use_sg, 0)
+ mpml_y = get_pml(Ny, dy, dt, c_ref, pml_y_size, multi_axial_pml_ratio * pml_y_alpha, False, 1)
+ mpml_y_sgy = get_pml(Ny, dy, dt, c_ref, pml_y_size, multi_axial_pml_ratio * pml_y_alpha, options.use_sg, 1)
+ mpml_z = get_pml(Nz, dz, dt, c_ref, pml_z_size, multi_axial_pml_ratio * pml_z_alpha, False, 2)
+ mpml_z_sgz = get_pml(Nz, dz, dt, c_ref, pml_z_size, multi_axial_pml_ratio * pml_z_alpha, options.use_sg, 2)
+
+ # define the k-space derivative operators, multiply by the staggered
+ # grid shift operators, and then re-order using scipy.fft.ifftshift (the option
+ # options.use_sg exists for debugging)
+ kx_vec = np.squeeze(k_sim.kgrid.k_vec[0])
+ ky_vec = np.squeeze(k_sim.kgrid.k_vec[1])
+ kz_vec = np.squeeze(k_sim.kgrid.k_vec[2])
+ if options.use_sg:
+ ddx_k_shift_pos = scipy.fft.ifftshift(1j * kx_vec * np.exp(1j * kx_vec * dx / 2.0))
+ ddy_k_shift_pos = scipy.fft.ifftshift(1j * ky_vec * np.exp(1j * ky_vec * dy / 2.0))
+ ddz_k_shift_pos = scipy.fft.ifftshift(1j * kz_vec * np.exp(1j * kz_vec * dz / 2.0))
+ ddx_k_shift_neg = scipy.fft.ifftshift(1j * kx_vec * np.exp(-1j * kx_vec * dx / 2.0))
+ ddy_k_shift_neg = scipy.fft.ifftshift(1j * ky_vec * np.exp(-1j * ky_vec * dy / 2.0))
+ ddz_k_shift_neg = scipy.fft.ifftshift(1j * kz_vec * np.exp(-1j * kz_vec * dz / 2.0))
+ else:
+ ddx_k_shift_pos = scipy.fft.ifftshift(1j * kx_vec)
+ ddx_k_shift_neg = scipy.fft.ifftshift(1j * kx_vec)
+ ddy_k_shift_pos = scipy.fft.ifftshift(1j * ky_vec)
+ ddy_k_shift_neg = scipy.fft.ifftshift(1j * ky_vec)
+ ddz_k_shift_pos = scipy.fft.ifftshift(1j * kz_vec)
+ ddz_k_shift_neg = scipy.fft.ifftshift(1j * kz_vec)
+
+ # force the derivative and shift operators to be in the correct direction for use with broadcasting
+ ddx_k_shift_pos = np.expand_dims(np.expand_dims(np.squeeze(ddx_k_shift_pos), axis=-1), axis=-1)
+ ddx_k_shift_neg = np.expand_dims(np.expand_dims(np.squeeze(ddx_k_shift_neg), axis=-1), axis=-1)
+ ddy_k_shift_pos = np.expand_dims(np.expand_dims(np.squeeze(ddy_k_shift_pos), axis=0), axis=-1)
+ ddy_k_shift_neg = np.expand_dims(np.expand_dims(np.squeeze(ddy_k_shift_neg), axis=0), axis=-1)
+ ddz_k_shift_pos = np.expand_dims(np.expand_dims(np.squeeze(ddz_k_shift_pos), axis=0), axis=0)
+ ddz_k_shift_neg = np.expand_dims(np.expand_dims(np.squeeze(ddz_k_shift_neg), axis=0), axis=0)
+
+ ddx_k_shift_pos = np.reshape(ddx_k_shift_pos, ddx_k_shift_pos.shape, order=myOrder)
+ ddx_k_shift_neg = np.reshape(ddx_k_shift_neg, ddx_k_shift_neg.shape, order=myOrder)
+ ddy_k_shift_pos = np.reshape(ddy_k_shift_pos, ddy_k_shift_pos.shape, order=myOrder)
+ ddy_k_shift_neg = np.reshape(ddy_k_shift_neg, ddy_k_shift_neg.shape, order=myOrder)
+ ddz_k_shift_pos = np.reshape(ddz_k_shift_pos, ddz_k_shift_pos.shape, order=myOrder)
+ ddz_k_shift_neg = np.reshape(ddz_k_shift_neg, ddz_k_shift_neg.shape, order=myOrder)
+
+ pml_x = np.transpose(pml_x)
+ pml_x_sgx = np.transpose(pml_x_sgx)
+ mpml_x = np.transpose(mpml_x)
+ mpml_x_sgx = np.transpose(mpml_x_sgx)
+ pml_x = np.expand_dims(pml_x, axis=-1)
+ pml_x_sgx = np.expand_dims(pml_x_sgx, axis=-1)
+ mpml_x = np.expand_dims(mpml_x, axis=-1)
+ mpml_x_sgx = np.expand_dims(mpml_x_sgx, axis=-1)
+
+ pml_y = np.expand_dims(pml_y, axis=0)
+ pml_y_sgy = np.expand_dims(pml_y_sgy, axis=0)
+ mpml_y = np.expand_dims(mpml_y, axis=0)
+ mpml_y_sgy = np.expand_dims(mpml_y_sgy, axis=0)
+
+ pml_z = np.expand_dims(pml_z, axis=0)
+ pml_z_sgz = np.expand_dims(pml_z_sgz, axis=0)
+ mpml_z = np.expand_dims(mpml_z, axis=0)
+ mpml_z_sgz = np.expand_dims(mpml_z_sgz, axis=0)
+
+ # =========================================================================
+ # DATA CASTING
+ # =========================================================================
+
+ # run subscript to cast the loop variables to the data type specified by data_cast
+ if not (options.data_cast == 'off'):
+ myType = np.float32
+ myCType = np.complex64
+ two = np.float32(2.0)
+ three = np.float32(3.0)
+ dt = np.float32(dt)
+ else:
+ myType = np.float64
+ myCType = np.complex128
+ two = np.float64(2.0)
+ three = np.float64(3.0)
+ dt = np.float64(dt)
+
+ grid_shape = (Nx, Ny, Nz)
+
+ # preallocate the loop variables using the castZeros anonymous function
+ # (this creates a matrix of zeros in the data type specified by data_cast)
+ ux_split_x = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ ux_split_y = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ ux_split_z = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ uy_split_x = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ uy_split_y = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ uy_split_z = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ uz_split_x = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ uz_split_y = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ uz_split_z = np.zeros(grid_shape, dtype=myType, order=myOrder)
+
+ sxx_split_x = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ sxx_split_y = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ sxx_split_z = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ syy_split_x = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ syy_split_y = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ syy_split_z = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ szz_split_x = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ szz_split_y = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ szz_split_z = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ sxy_split_x = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ sxy_split_y = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ sxz_split_x = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ sxz_split_z = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ syz_split_y = np.zeros(grid_shape, dtype=myType, order=myOrder)
+ syz_split_z = np.zeros(grid_shape, dtype=myType, order=myOrder)
+
+ ux_sgx = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ uy_sgy = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ uz_sgz = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ duxdx = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ duxdy = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ duxdz = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ duydx = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ duydy = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ duydz = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ duzdx = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ duzdy = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ duzdz = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ dsxxdx = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dsyydy = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dszzdz = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ dsxydx = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dsxydy = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ dsxzdx = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dsxzdz = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ dsyzdy = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dsyzdz = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ p = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ if options.kelvin_voigt_model:
+ dduxdxdt = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dduxdydt = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dduxdzdt = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ dduydxdt = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dduydydt = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dduydzdt = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ dduzdxdt = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dduzdydt = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+ dduzdzdt = np.zeros(grid_shape, dtype=myType, order=myOrder) # **
+
+ # to save memory, the variables noted with a ** do not neccesarily need to
+ # be explicitly stored (they are not needed for update steps). Instead they
+ # could be replaced with a small number of temporary variables that are
+ # reused several times during the time loop.
+
+
+ # =========================================================================
+ # CREATE INDEX VARIABLES
+ # =========================================================================
+
+ # setup the time index variable
+ if not options.time_rev:
+ index_start: int = 0
+ index_step: int = 1
+ index_end: int = Nt
+ else:
+ # throw error for unsupported feature
+ raise TypeError('Time reversal using sensor.time_reversal_boundary_data is not currently supported.')
+
+ # =========================================================================
+ # ENSURE PYTHON INDEXING
+ # =========================================================================
+
+ # These should be zero indexed
+ if hasattr(k_sim, 's_source_pos_index'):
+ if k_sim.s_source_pos_index is not None:
+ # if k_sim.s_source_pos_index.ndim != 0:
+ k_sim.s_source_pos_index = np.squeeze(np.asarray(k_sim.s_source_pos_index)) - int(1)
+
+ if hasattr(k_sim, 'u_source_pos_index'):
+ if k_sim.u_source_pos_index is not None:
+ # if k_sim.u_source_pos_index.ndim != 0:
+ k_sim.u_source_pos_index = np.squeeze(k_sim.u_source_pos_index) - int(1)
+
+ if hasattr(k_sim, 'p_source_pos_index'):
+ if k_sim.p_source_pos_index is not None:
+ # if k_sim.p_source_pos_index.ndim != 0:
+ k_sim.p_source_pos_index = np.squeeze(k_sim.p_source_pos_index) - int(1)
+
+ if hasattr(k_sim, 's_source_sig_index'):
+ if k_sim.s_source_sig_index is not None:
+ k_sim.s_source_sig_index = np.squeeze(k_sim.s_source_sig_index) - int(1)
+
+ if hasattr(k_sim, 'u_source_sig_index') and k_sim.u_source_sig_index is not None:
+ k_sim.u_source_sig_index = np.squeeze(k_sim.u_source_sig_index) - int(1)
+
+ if hasattr(k_sim, 'p_source_sig_index') and k_sim.p_source_sig_index is not None:
+ k_sim.p_source_sig_index = np.squeeze(k_sim.p_source_sig_index) - int(1)
+
+ if hasattr(k_sim, 'sensor_mask_index') and k_sim.sensor_mask_index is not None:
+ k_sim.sensor_mask_index = np.squeeze(k_sim.sensor_mask_index) - int(1)
+
+ # These should be zero indexed. Note the x2, y2 and z2 indices do not need to be shifted
+ if hasattr(record, 'x1_inside') and record.x1_inside is not None:
+ if (record.x1_inside == 0):
+ print("GAH")
+ else:
+ record.x1_inside = int(record.x1_inside - 1)
+
+ if hasattr(record, 'y1_inside') and record.y1_inside is not None:
+ record.y1_inside = int(record.y1_inside - 1)
+
+ if hasattr(record, 'z1_inside') and record.z1_inside is not None:
+ record.z1_inside = int(record.z1_inside - 1)
+
+ sensor.record_start_index: int = sensor.record_start_index - int(1)
+
+
+ # =========================================================================
+ # CASTING
+ # =========================================================================
+
+ ddx_k_shift_pos = ddx_k_shift_pos.astype(myCType)
+ ddx_k_shift_neg = ddx_k_shift_neg.astype(myCType)
+
+ ddy_k_shift_pos = ddy_k_shift_pos.astype(myCType)
+ ddy_k_shift_neg = ddy_k_shift_neg.astype(myCType)
+
+ ddz_k_shift_pos = ddz_k_shift_pos.astype(myCType)
+ ddz_k_shift_neg = ddz_k_shift_neg.astype(myCType)
+
+ ux_split_x = ux_split_x.astype(myType)
+ ux_split_y = ux_split_y.astype(myType)
+ ux_split_z = ux_split_z.astype(myType)
+ uy_split_x = uy_split_x.astype(myType)
+ uy_split_y = uy_split_y.astype(myType)
+ uy_split_z = uy_split_z.astype(myType)
+ uz_split_x = uz_split_x.astype(myType)
+ uz_split_y = uz_split_y.astype(myType)
+ uz_split_z = uz_split_z.astype(myType)
+
+ ux_sgx = ux_sgx.astype(myType)
+ uy_sgy = uy_sgy.astype(myType)
+ uz_sgz = uz_sgz.astype(myType)
+
+ mpml_x = mpml_x.astype(myType)
+ mpml_y = mpml_y.astype(myType)
+ mpml_z = mpml_z.astype(myType)
+ pml_x = pml_x.astype(myType)
+ pml_y = pml_y.astype(myType)
+ pml_z = pml_z.astype(myType)
+
+ pml_x_sgx = pml_x_sgx.astype(myType)
+ pml_y_sgy = pml_y_sgy.astype(myType)
+ pml_z_sgz = pml_z_sgz.astype(myType)
+ mpml_x_sgx = mpml_x_sgx.astype(myType)
+ mpml_y_sgy = mpml_y_sgy.astype(myType)
+ mpml_z_sgz = mpml_z_sgz.astype(myType)
+
+ rho0_sgx_inv = rho0_sgx_inv.astype(myType)
+ rho0_sgy_inv = rho0_sgy_inv.astype(myType)
+ rho0_sgz_inv = rho0_sgz_inv.astype(myType)
+
+ duxdx = duxdx.astype(myType)
+ duxdy = duxdy.astype(myType)
+ duxdz = duxdz.astype(myType)
+
+ duydx = duydx.astype(myType)
+ duydy = duydy.astype(myType)
+ duydz = duydz.astype(myType)
+
+ duzdx = duzdx.astype(myType)
+ duzdy = duzdy.astype(myType)
+ duzdz = duzdz.astype(myType)
+
+ dsxxdx = dsxxdx.astype(myType)
+ dsyydy = dsyydy.astype(myType)
+ dszzdz = dszzdz.astype(myType)
+ dsxydx = dsxydx.astype(myType)
+ dsxydy = dsxydy.astype(myType)
+ dsxzdx = dsxzdx.astype(myType)
+ dsxzdz = dsxzdz.astype(myType)
+ dsyzdy = dsyzdy.astype(myType)
+ dsyzdz = dsyzdz.astype(myType)
+
+ if m_mu == 3:
+ mu = mu.astype(myType)
+ lame_lambda = lame_lambda.astype(myType)
+ else:
+ if not (options.data_cast == 'off'):
+ mu = np.float32(mu)
+ lame_lambda = np.float32(lame_lambda)
+ mu_sgxy = np.float32(mu_sgxy)
+ mu_sgxz = np.float32(mu_sgxz)
+ mu_sgyz = np.float32(mu_sgyz)
+ else:
+ mu = np.float64(mu)
+ lame_lambda = np.float64(lame_lambda)
+ mu_sgxy = np.float64(mu_sgxy)
+ mu_sgxz = np.float64(mu_sgxz)
+ mu_sgyz = np.float64(mu_sgyz)
+
+ p = p.astype(myType)
+
+ if options.kelvin_voigt_model:
+ if m_eta == 3:
+ chi = chi.astype(myType)
+ eta = eta.astype(myType)
+ else:
+ if not (options.data_cast == 'off'):
+ chi = np.float32(chi)
+ eta = np.float32(eta)
+ eta_sgxy = np.float32(eta_sgxy)
+ eta_sgxz = np.float32(eta_sgxz)
+ eta_sgyz = np.float32(eta_sgyz)
+ else:
+ chi = np.float64(chi)
+ eta = np.float64(eta)
+ eta_sgxy = np.float64(eta_sgxy)
+ eta_sgxz = np.float64(eta_sgxz)
+ eta_sgyz = np.float64(eta_sgyz)
+ dduxdxdt = dduxdxdt.astype(myType)
+ dduxdydt = dduxdydt.astype(myType)
+ dduxdzdt = dduxdzdt.astype(myType)
+ dduydxdt = dduydxdt.astype(myType)
+ dduydydt = dduydydt.astype(myType)
+ dduydzdt = dduydzdt.astype(myType)
+ dduzdxdt = dduzdxdt.astype(myType)
+ dduzdydt = dduzdydt.astype(myType)
+ dduzdzdt = dduzdzdt.astype(myType)
+
+
+ # =========================================================================
+ # LOOP THROUGH TIME STEPS
+ # =========================================================================
+
+ # update command line status
+
+ # update command line status
+ t0 = timer.toc()
+ t0_scale = scale_time(t0)
+ print('\tprecomputation completed in', t0_scale)
+ print('\tstarting time loop...')
+
+ # start time loop
+ for t_index in tqdm(np.arange(index_start, index_end, index_step, dtype=int)):
+
+ dsxxdx = np.real(scipy.fft.ifftn(np.multiply(ddx_k_shift_pos, scipy.fft.fftn(sxx_split_x + sxx_split_y + sxx_split_z, axes=(0,) )), axes=(0,) ))
+ dsyydy = np.real(scipy.fft.ifftn(np.multiply(ddy_k_shift_pos, scipy.fft.fftn(syy_split_x + syy_split_y + syy_split_z, axes=(1,) )), axes=(1,) ))
+ dszzdz = np.real(scipy.fft.ifftn(np.multiply(ddz_k_shift_pos, scipy.fft.fftn(szz_split_x + szz_split_y + szz_split_z, axes=(2,) )), axes=(2,) ))
+
+ temp = sxy_split_x + sxy_split_y
+ dsxydx = np.real(scipy.fft.ifftn(np.multiply(ddx_k_shift_neg, scipy.fft.fftn(temp, axes=(0,) )), axes=(0,) ))
+ dsxydy = np.real(scipy.fft.ifftn(np.multiply(ddy_k_shift_neg, scipy.fft.fftn(temp, axes=(1,) )), axes=(1,) ))
+
+ temp = sxz_split_x + sxz_split_z
+ dsxzdx = np.real(scipy.fft.ifftn(np.multiply(ddx_k_shift_neg, scipy.fft.fftn(temp, axes=(0,) )), axes=(0,) ))
+ dsxzdz = np.real(scipy.fft.ifftn(np.multiply(ddz_k_shift_neg, scipy.fft.fftn(temp, axes=(2,) )), axes=(2,) ))
+
+ temp = syz_split_y + syz_split_z
+ dsyzdy = np.real(scipy.fft.ifftn(np.multiply(ddy_k_shift_neg, scipy.fft.fftn(temp, axes=(1,) )), axes=(1,) ))
+ dsyzdz = np.real(scipy.fft.ifftn(np.multiply(ddz_k_shift_neg, scipy.fft.fftn(temp, axes=(2,) )), axes=(2,) ))
+
+ # calculate the split-field components of ux_sgx, uy_sgy, and uz_sgz at the next time step using the components of the stress at the current
+ # time step
+
+ ux_split_x = mpml_z * mpml_y * pml_x_sgx * (mpml_z * mpml_y * pml_x_sgx * ux_split_x + dt * rho0_sgx_inv * dsxxdx)
+
+ ux_split_y = mpml_x_sgx * mpml_z * pml_y * (mpml_x_sgx * mpml_z * pml_y * ux_split_y + dt * rho0_sgx_inv * dsxydy)
+
+ ux_split_z = mpml_y * mpml_x_sgx * pml_z * (mpml_y * mpml_x_sgx * pml_z * ux_split_z + dt * rho0_sgx_inv * dsxzdz)
+
+ uy_split_x = mpml_z * mpml_y_sgy * pml_x * (mpml_z * mpml_y_sgy * pml_x * uy_split_x + dt * rho0_sgy_inv * dsxydx)
+
+ uy_split_y = mpml_x * mpml_z * pml_y_sgy * (mpml_x * mpml_z * pml_y_sgy * uy_split_y + dt * rho0_sgy_inv * dsyydy)
+
+ uy_split_z = mpml_y_sgy * mpml_x * pml_z * (mpml_y_sgy * mpml_x * pml_z * uy_split_z + dt * rho0_sgy_inv * dsyzdz)
+
+ uz_split_x = mpml_z_sgz * mpml_y * pml_x * (mpml_z_sgz * mpml_y * pml_x * uz_split_x + dt * rho0_sgz_inv * dsxzdx)
+
+ uz_split_y = mpml_x * mpml_z_sgz * pml_y * (mpml_x * mpml_z_sgz * pml_y * uz_split_y + dt * rho0_sgz_inv * dsyzdy)
+
+ uz_split_z = mpml_y * mpml_x * pml_z_sgz * (mpml_y * mpml_x * pml_z_sgz * uz_split_z + dt * rho0_sgz_inv * dszzdz)
+
+ # add in the velocity source terms
+ if k_sim.source_ux is not False and k_sim.source_ux >= t_index:
+ if (source.u_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ ux_split_x[np.unravel_index(k_sim.u_source_pos_index, ux_split_x.shape, order=myOrder)] = np.squeeze(k_sim.source.ux[k_sim.u_source_sig_index, t_index])
+ else:
+ # add the source values to the existing field values
+ ux_split_x[np.unravel_index(k_sim.u_source_pos_index, ux_split_x.shape, order=myOrder)] += np.squeeze(k_sim.source.ux[k_sim.u_source_sig_index, t_index])
+
+ if k_sim.source_uy is not False and k_sim.source_uy >= t_index:
+ if (source.u_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ uy_split_y[np.unravel_index(k_sim.u_source_pos_index, uy_split_y.shape, order=myOrder)] = np.squeeze(k_sim.source.uy[k_sim.u_source_sig_index, t_index])
+ else:
+ # add the source values to the existing field values
+ uy_split_y[np.unravel_index(k_sim.u_source_pos_index, uy_split_y.shape, order=myOrder)] += np.squeeze(k_sim.source.uy[k_sim.u_source_sig_index, t_index])
+
+ if k_sim.source_uz is not False and k_sim.source_uz >= t_index:
+ if (source.u_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ uz_split_z[np.unravel_index(k_sim.u_source_pos_index, uz_split_z.shape, order=myOrder)] = np.squeeze(k_sim.source.uz[k_sim.u_source_sig_index, t_index])
+ else:
+ # add the source values to the existing field values
+ uz_split_z[np.unravel_index(k_sim.u_source_pos_index, uz_split_z.shape, order=myOrder)] += np.squeeze(k_sim.source.uz[k_sim.u_source_sig_index, t_index])
+
+ ############
+
+ # combine split field components
+ # these variables do not necessarily need to be stored, they could be computed when needed)
+
+ ux_sgx = ux_split_x + ux_split_y + ux_split_z
+ uy_sgy = uy_split_x + uy_split_y + uy_split_z
+ uz_sgz = uz_split_x + uz_split_y + uz_split_z
+
+ ############
+
+ # calculate the velocity gradients
+ # these variables do not necessarily need to be stored, they could be computed when needed
+ duxdx = np.real(scipy.fft.ifftn(np.multiply(ddx_k_shift_neg, scipy.fft.fftn(ux_sgx, axes=(0,) ), order=myOrder), axes=(0,) ))
+ duxdy = np.real(scipy.fft.ifftn(np.multiply(ddy_k_shift_pos, scipy.fft.fftn(ux_sgx, axes=(1,) ), order=myOrder), axes=(1,) ))
+ duxdz = np.real(scipy.fft.ifftn(np.multiply(ddz_k_shift_pos, scipy.fft.fftn(ux_sgx, axes=(2,) ), order=myOrder), axes=(2,) ))
+
+ duydx = np.real(scipy.fft.ifftn(np.multiply(ddx_k_shift_pos, scipy.fft.fftn(uy_sgy, axes=(0,) ), order=myOrder), axes=(0,) ))
+ duydy = np.real(scipy.fft.ifftn(np.multiply(ddy_k_shift_neg, scipy.fft.fftn(uy_sgy, axes=(1,) ), order=myOrder), axes=(1,) ))
+ duydz = np.real(scipy.fft.ifftn(np.multiply(ddz_k_shift_pos, scipy.fft.fftn(uy_sgy, axes=(2,) ), order=myOrder), axes=(2,) ))
+
+ duzdx = np.real(scipy.fft.ifftn(np.multiply(ddx_k_shift_pos, scipy.fft.fftn(uz_sgz, axes=(0,) ), order=myOrder), axes=(0,) ))
+ duzdy = np.real(scipy.fft.ifftn(np.multiply(ddy_k_shift_pos, scipy.fft.fftn(uz_sgz, axes=(1,) ), order=myOrder), axes=(1,) ))
+ duzdz = np.real(scipy.fft.ifftn(np.multiply(ddz_k_shift_neg, scipy.fft.fftn(uz_sgz, axes=(2,) ), order=myOrder), axes=(2,) ))
+
+ if options.kelvin_voigt_model:
+
+ # compute additional gradient terms needed for the Kelvin-Voigt model
+ temp = np.multiply((dsxxdx + dsxydy + dsxzdz), rho0_sgx_inv)
+ dduxdxdt = np.real(scipy.fft.ifftn(np.multiply(ddx_k_shift_neg, scipy.fft.fftn(temp, axes=(0,) ), order=myOrder), axes=(0,) ))
+ dduxdydt = np.real(scipy.fft.ifftn(np.multiply(ddy_k_shift_pos, scipy.fft.fftn(temp, axes=(1,) ), order=myOrder), axes=(1,) ))
+ dduxdzdt = np.real(scipy.fft.ifftn(np.multiply(ddz_k_shift_pos, scipy.fft.fftn(temp, axes=(2,) ), order=myOrder), axes=(2,) ))
+
+ temp = np.multiply((dsxydx + dsyydy + dsyzdz), rho0_sgy_inv)
+ dduydxdt = np.real(scipy.fft.ifftn(np.multiply(ddx_k_shift_pos, scipy.fft.fftn(temp, axes=(0,) ), order=myOrder), axes=(0,) ))
+ dduydydt = np.real(scipy.fft.ifftn(np.multiply(ddy_k_shift_neg, scipy.fft.fftn(temp, axes=(1,) ), order=myOrder), axes=(1,) ))
+ dduydzdt = np.real(scipy.fft.ifftn(np.multiply(ddz_k_shift_pos, scipy.fft.fftn(temp, axes=(2,) ), order=myOrder), axes=(2,) ))
+
+ temp = np.multiply((dsxzdx + dsyzdy + dszzdz), rho0_sgz_inv)
+ dduzdxdt = np.real(scipy.fft.ifftn(np.multiply(ddx_k_shift_pos, scipy.fft.fftn(temp, axes=(0,) ), order=myOrder), axes=(0,) ))
+ dduzdydt = np.real(scipy.fft.ifftn(np.multiply(ddy_k_shift_pos, scipy.fft.fftn(temp, axes=(1,) ), order=myOrder), axes=(1,) ))
+ dduzdzdt = np.real(scipy.fft.ifftn(np.multiply(ddz_k_shift_neg, scipy.fft.fftn(temp, axes=(2,) ), order=myOrder), axes=(2,) ))
+
+ # update the normal shear components of the stress tensor using a Kelvin-Voigt model with a split-field multi-axial pml
+
+ # split_x
+ temp = mpml_z * mpml_y * pml_x
+ temp1 = dt * (lame_lambda * duxdx + chi * dduxdxdt)
+ temp2 = dt * two * (mu * duxdx + eta * dduxdxdt)
+
+ sxx_split_x = temp * (temp * sxx_split_x + temp1 + temp2)
+ syy_split_x = temp * (temp * syy_split_x + temp1)
+ szz_split_x = temp * (temp * szz_split_x + temp1)
+
+ # split_y
+ temp = mpml_x * mpml_z * pml_y
+ temp1 = dt * (lame_lambda * duydy + chi * dduydydt)
+ temp2 = dt * two * (mu * duydy + eta * dduydydt)
+
+ sxx_split_y = temp * (temp * sxx_split_y + temp1)
+ syy_split_y = temp * (temp * syy_split_y + temp1 + temp2)
+ szz_split_y = temp * (temp * szz_split_y + temp1)
+
+ # split_z
+ temp = mpml_y * mpml_x * pml_z
+ temp1 = dt * (lame_lambda * duzdz + chi * dduzdzdt)
+ temp2 = dt * two * (mu * duzdz + eta * dduzdzdt)
+
+ sxx_split_z = temp * (temp * sxx_split_z + temp1)
+ syy_split_z = temp * (temp * syy_split_z + temp1)
+ szz_split_z = temp * (temp * szz_split_z + temp1 + temp2)
+
+ temp = mpml_z * mpml_y_sgy * pml_x_sgx
+ sxy_split_x = temp * (temp * sxy_split_x + dt * (mu_sgxy * duydx + eta_sgxy * dduydxdt))
+
+ temp = mpml_z * mpml_x_sgx * pml_y_sgy
+ sxy_split_y = temp * (temp * sxy_split_y + dt * (mu_sgxy * duxdy + eta_sgxy * dduxdydt))
+
+ temp = mpml_y * mpml_z_sgz * pml_x_sgx
+ sxz_split_x = temp * (temp * sxz_split_x + dt * (mu_sgxz * duzdx + eta_sgxz * dduzdxdt))
+
+ temp = mpml_y * mpml_x_sgx * pml_z_sgz
+ sxz_split_z = temp * (temp * sxz_split_z + dt * (mu_sgxz * duxdz + eta_sgxz * dduxdzdt))
+
+ temp = mpml_x * mpml_z_sgz * pml_y_sgy
+ syz_split_y = temp * (temp * syz_split_y + dt * (mu_sgyz * duzdy + eta_sgyz * dduzdydt))
+
+ temp = mpml_x * mpml_y_sgy * pml_z_sgz
+ syz_split_z = temp * (temp * syz_split_z + dt * (mu_sgyz * duydz + eta_sgyz * dduydzdt))
+
+ else:
+
+ temp1 = dt * lame_lambda
+ temp2 = dt * two * mu
+
+ temp = mpml_z * mpml_y * pml_x
+ sxx_split_x = temp * (temp * sxx_split_x + temp1 * duxdx + temp2 * duxdx)
+ syy_split_x = temp * (temp * syy_split_x + temp1 * duxdx)
+ szz_split_x = temp * (temp * szz_split_x + temp1 * duxdx)
+
+ temp = mpml_x * mpml_z * pml_y
+ sxx_split_y = temp * (temp * sxx_split_y + temp1 * duydy)
+ syy_split_y = temp * (temp * syy_split_y + temp1 * duydy + temp2 * duydy)
+ szz_split_y = temp * (temp * szz_split_y + temp1 * duydy)
+
+ temp = mpml_y * mpml_x * pml_z
+ sxx_split_z = temp * (temp * sxx_split_z + temp1 * duzdz)
+ syy_split_z = temp * (temp * syy_split_z + temp1 * duzdz)
+ szz_split_z = temp * (temp * szz_split_z + temp1 * duzdz + temp2 * duzdz)
+
+
+ sxy_split_x = mpml_z * (mpml_y_sgy * (pml_x_sgx * (mpml_z * (mpml_y_sgy * (pml_x_sgx * sxy_split_x)) + \
+ dt * mu_sgxy * duydx)))
+
+ sxy_split_y = mpml_z * (mpml_x_sgx * (pml_y_sgy * (mpml_z * (mpml_x_sgx * (pml_y_sgy * sxy_split_y)) + \
+ dt * mu_sgxy * duxdy)))
+
+ sxz_split_x = mpml_y * (mpml_z_sgz * (pml_x_sgx * (mpml_y * (mpml_z_sgz * (pml_x_sgx * sxz_split_x)) + \
+ dt * mu_sgxz * duzdx)))
+
+ sxz_split_z = mpml_y * (mpml_x_sgx * (pml_z_sgz * (mpml_y * (mpml_x_sgx * (pml_z_sgz * sxz_split_z)) + \
+ dt * mu_sgxz * duxdz)))
+
+ syz_split_y = mpml_x * (mpml_z_sgz * (pml_y_sgy * (mpml_x * (mpml_z_sgz * (pml_y_sgy * syz_split_y)) + \
+ dt * mu_sgyz * duzdy)))
+
+ syz_split_z = mpml_x * (mpml_y_sgy * (pml_z_sgz * (mpml_x * (mpml_y_sgy * (pml_z_sgz * syz_split_z)) + \
+ dt * mu_sgyz * duydz)))
+
+
+ if (k_sim.source_sxx is not False and t_index < np.shape(source.sxx)[1]):
+ if (source.s_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ sxx_split_x[np.unravel_index(k_sim.s_source_pos_index, sxx_split_x.shape, order=myOrder)] = k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+ sxx_split_y[np.unravel_index(k_sim.s_source_pos_index, sxx_split_y.shape, order=myOrder)] = k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+ sxx_split_z[np.unravel_index(k_sim.s_source_pos_index, sxx_split_z.shape, order=myOrder)] = k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+ else:
+ # add the source values to the existing field values
+ sxx_split_x[np.unravel_index(k_sim.s_source_pos_index, sxx_split_x.shape, order=myOrder)] += k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+ sxx_split_y[np.unravel_index(k_sim.s_source_pos_index, sxx_split_y.shape, order=myOrder)] += k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+ sxx_split_z[np.unravel_index(k_sim.s_source_pos_index, sxx_split_z.shape, order=myOrder)] += k_sim.source.sxx[k_sim.s_source_sig_index, t_index]
+
+ if (k_sim.source_syy is not False and t_index < np.shape(source.syy)[1]):
+ if (source.s_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ syy_split_x[np.unravel_index(k_sim.s_source_pos_index, syy_split_x.shape, order=myOrder)] = k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+ syy_split_y[np.unravel_index(k_sim.s_source_pos_index, syy_split_y.shape, order=myOrder)] = k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+ syy_split_z[np.unravel_index(k_sim.s_source_pos_index, syy_split_z.shape, order=myOrder)] = k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+ else:
+ # add the source values to the existing field values
+ syy_split_x[np.unravel_index(k_sim.s_source_pos_index, syy_split_x.shape, order=myOrder)] += k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+ syy_split_y[np.unravel_index(k_sim.s_source_pos_index, syy_split_y.shape, order=myOrder)] += k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+ syy_split_z[np.unravel_index(k_sim.s_source_pos_index, syy_split_z.shape, order=myOrder)] += k_sim.source.syy[k_sim.s_source_sig_index, t_index]
+
+ if (k_sim.source_szz is not False and t_index < np.shape(source.szz)[1]):
+ if (source.s_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ szz_split_x[np.unravel_index(k_sim.s_source_pos_index, szz_split_x.shape, order=myOrder)] = k_sim.source.szz[k_sim.s_source_sig_index, t_index]
+ szz_split_y[np.unravel_index(k_sim.s_source_pos_index, szz_split_y.shape, order=myOrder)] = k_sim.source.szz[k_sim.s_source_sig_index, t_index]
+ szz_split_z[np.unravel_index(k_sim.s_source_pos_index, szz_split_z.shape, order=myOrder)] = k_sim.source.szz[k_sim.s_source_sig_index, t_index]
+ else:
+ # add the source values to the existing field values
+ szz_split_x[np.unravel_index(k_sim.s_source_pos_index, szz_split_x.shape, order=myOrder)] += k_sim.source.szz[k_sim.s_source_sig_index, t_index]
+ szz_split_y[np.unravel_index(k_sim.s_source_pos_index, szz_split_y.shape, order=myOrder)] += k_sim.source.szz[k_sim.s_source_sig_index, t_index]
+ szz_split_z[np.unravel_index(k_sim.s_source_pos_index, szz_split_z.shape, order=myOrder)] += k_sim.source.szz[k_sim.s_source_sig_index, t_index]
+
+ if (k_sim.source_sxy is not False and t_index < np.shape(source.sxy)[1]):
+ if (source.s_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ sxy_split_x[np.unravel_index(k_sim.s_source_pos_index, sxy_split_x.shape, order=myOrder)] = k_sim.source.sxy[k_sim.s_source_sig_index, t_index]
+ sxy_split_y[np.unravel_index(k_sim.s_source_pos_index, sxy_split_y.shape, order=myOrder)] = k_sim.source.sxy[k_sim.s_source_sig_index, t_index]
+ else:
+ # add the source values to the existing field values
+ sxy_split_x[np.unravel_index(k_sim.s_source_pos_index, sxy_split_x.shape, order=myOrder)] += k_sim.source.sxy[k_sim.s_source_sig_index, t_index]
+ sxy_split_y[np.unravel_index(k_sim.s_source_pos_index, sxy_split_y.shape, order=myOrder)] += k_sim.source.sxy[k_sim.s_source_sig_index, t_index]
+
+ if (k_sim.source_sxz is not False and t_index < np.shape(source.sxz)[1]):
+ if (source.s_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ sxz_split_x[np.unravel_index(k_sim.s_source_pos_index, sxz_split_x.shape, order=myOrder)] = k_sim.source.sxz[k_sim.s_source_sig_index, t_index]
+ sxz_split_z[np.unravel_index(k_sim.s_source_pos_index, sxz_split_z.shape, order=myOrder)] = k_sim.source.sxz[k_sim.s_source_sig_index, t_index]
+ else:
+ # add the source values to the existing field values
+ sxz_split_x[np.unravel_index(k_sim.s_source_pos_index, sxz_split_x.shape, order=myOrder)] += k_sim.source.sxz[k_sim.s_source_sig_index, t_index]
+ sxz_split_z[np.unravel_index(k_sim.s_source_pos_index, sxz_split_z.shape, order=myOrder)] += k_sim.source.sxz[k_sim.s_source_sig_index, t_index]
+
+ if (k_sim.source_syz is not False and t_index < np.shape(source.syz)[1]):
+ if (source.s_mode == 'dirichlet'):
+ # enforce the source values as a dirichlet boundary condition
+ syz_split_y[np.unravel_index(k_sim.s_source_pos_index, syz_split_y.shape, order=myOrder)] = k_sim.source.syz[k_sim.s_source_sig_index, t_index]
+ syz_split_z[np.unravel_index(k_sim.s_source_pos_index, syz_split_y.shape, order=myOrder)] = k_sim.source.syz[k_sim.s_source_sig_index, t_index]
+ else:
+ # add the source values to the existing field values
+ syz_split_y[np.unravel_index(k_sim.s_source_pos_index, syz_split_y.shape, order=myOrder)] += k_sim.source.syz[k_sim.s_source_sig_index, t_index]
+ syz_split_z[np.unravel_index(k_sim.s_source_pos_index, syz_split_z.shape, order=myOrder)] += k_sim.source.syz[k_sim.s_source_sig_index, t_index]
+
+ # compute pressure from the normal components of the stress
+ p = -(sxx_split_x + sxx_split_y + sxx_split_z +
+ syy_split_x + syy_split_y + syy_split_z +
+ szz_split_x + szz_split_y + szz_split_z) / three
+
+
+ # extract required sensor data from the pressure and particle velocity
+ # fields if the number of time steps elapsed is greater than
+ # sensor.record_start_index (now defaults to 0)
+ if ((k_sim.use_sensor is not False) and (not k_sim.elastic_time_rev) and (t_index >= sensor.record_start_index)):
+
+ # update index for data storage
+ file_index: int = t_index - sensor.record_start_index
+
+ # run sub-function to extract the required data
+ extract_options = dotdict({'record_u_non_staggered': k_sim.record.u_non_staggered,
+ 'record_u_split_field': k_sim.record.u_split_field,
+ 'record_I': k_sim.record.I,
+ 'record_I_avg': k_sim.record.I_avg,
+ 'binary_sensor_mask': k_sim.binary_sensor_mask,
+ 'record_p': k_sim.record.p,
+ 'record_p_max': k_sim.record.p_max,
+ 'record_p_min': k_sim.record.p_min,
+ 'record_p_rms': k_sim.record.p_rms,
+ 'record_p_max_all': k_sim.record.p_max_all,
+ 'record_p_min_all': k_sim.record.p_min_all,
+ 'record_u': k_sim.record.u,
+ 'record_u_max': k_sim.record.u_max,
+ 'record_u_min': k_sim.record.u_min,
+ 'record_u_rms': k_sim.record.u_rms,
+ 'record_u_max_all': k_sim.record.u_max_all,
+ 'record_u_min_all': k_sim.record.u_min_all,
+ 'compute_directivity': False})
+
+ sensor_data = extract_sensor_data(3, sensor_data, file_index, k_sim.sensor_mask_index,
+ extract_options, k_sim.record, p, ux_sgx, uy_sgy, uz_sgz)
+
+ # update command line status
+ t1 = timer.toc()
+ t1_scale = scale_time(t1)
+ print('\tsimulation completed in', t1_scale)
+
+
+ # =========================================================================
+ # CLEAN UP
+ # =========================================================================
+
+ # options.cuboid_corners
+
+ if not options.cuboid_corners:
+ # save the final acoustic pressure if required
+ if k_sim.record.p_final or options.elastic_time_rev:
+ sensor_data.p_final = p[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ # save the final particle velocity if required
+ if k_sim.record.u_final:
+ sensor_data.ux_final = ux_sgx[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ sensor_data.uy_final = uy_sgy[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ sensor_data.uz_final = uz_sgz[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ else:
+ # save the final acoustic pressure if required
+ if k_sim.record.p_final or options.elastic_time_rev:
+ sensor_data.append(dotdict({'p_final': p[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]}))
+ # save the final particle velocity if required
+ if k_sim.record.u_final:
+ i: int = len(sensor_data) - 1
+ sensor_data[i].ux_final = ux_sgx[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ sensor_data[i].uy_final = uy_sgy[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ sensor_data[i].uz_final = uz_sgz[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]
+ elif k_sim.record.u_final:
+ sensor_data.append(dotdict({'ux_final': ux_sgx[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside],
+ 'uy_final': uy_sgy[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside],
+ 'uz_final': uz_sgz[record.x1_inside:record.x2_inside,
+ record.y1_inside:record.y2_inside,
+ record.z1_inside:record.z2_inside]}))
+
+ # # run subscript to cast variables back to double precision if required
+ # if options.data_recast:
+ # kspaceFirstOrder_dataRecast
+
+ # # run subscript to compute and save intensity values
+ if options.use_sensor and not options.elastic_time_rev and (k_sim.record.I or k_sim.record.I_avg):
+ save_intensity_options = dotdict({'record_I_avg': k_sim.record.I_avg,
+ 'record_p': k_sim.record.p,
+ 'record_I': k_sim.record.I,
+ 'record_u_non_staggered': k_sim.record.u_non_staggered,
+ 'use_cuboid_corners': options.cuboid_corners})
+ sensor_data = save_intensity(kgrid, sensor_data, save_intensity_options)
+
+ # reorder the sensor points if a binary sensor mask was used for Cartesian
+ # sensor mask nearest neighbour interpolation (this is performed after
+ # recasting as the GPU toolboxes do not all support this subscript)
+ if options.use_sensor and k_sim.reorder_data:
+ print("reorder?")
+ sensor_data = reorder_sensor_data(kgrid, sensor, deepcopy(sensor_data))
+
+ # filter the recorded time domain pressure signals if transducer filter
+ # parameters are given
+ if options.use_sensor and (not options.elastic_time_rev) and k_sim.sensor.frequency_response is not None:
+ sensor_data.p = gaussian_filter(sensor_data.p, 1.0 / dt,
+ k_sim.sensor.frequency_response[0], k_sim.sensor.frequency_response[1])
+
+ # reorder the sensor points if cuboid corners is used (outputs are indexed
+ # as [X, Y, Z, T] or [X, Y, Z] rather than [sensor_index, time_index]
+ num_stream_time_points: int = k_sim.kgrid.Nt - k_sim.sensor.record_start_index
+ if options.cuboid_corners:
+ print("cuboid corners?")
+ time_info = dotdict({'num_stream_time_points': num_stream_time_points,
+ 'num_recorded_time_points': k_sim.num_recorded_time_points,
+ 'stream_to_disk': options.stream_to_disk})
+ cuboid_info = dotdict({'record_p': k_sim.record.p,
+ 'record_p_rms': k_sim.record.p_rms,
+ 'record_p_max': k_sim.record.p_max,
+ 'record_p_min': k_sim.record.p_min,
+ 'record_p_final': k_sim.record.p_final,
+ 'record_p_max_all': k_sim.record.p_max_all,
+ 'record_p_min_all': k_sim.record.p_min_all,
+ 'record_u': k_sim.record.u,
+ 'record_u_non_staggered': k_sim.record.u_non_staggered,
+ 'record_u_rms': k_sim.record.u_rms,
+ 'record_u_max': k_sim.record.u_max,
+ 'record_u_min': k_sim.record.u_min,
+ 'record_u_final': k_sim.record.u_final,
+ 'record_u_max_all': k_sim.record.u_max_all,
+ 'record_u_min_all': k_sim.record.u_min_all,
+ 'record_I': k_sim.record.I,
+ 'record_I_avg': k_sim.record.I_avg})
+ sensor_data = reorder_cuboid_corners(k_sim.kgrid, k_sim.record, sensor_data, time_info, cuboid_info, verbose=True)
+
+ if options.elastic_time_rev:
+ # if computing time reversal, reassign sensor_data.p_final to sensor_data
+ # sensor_data = sensor_data.p_final
+ raise NotImplementedError("elastic_time_rev is not implemented")
+ elif not options.use_sensor:
+ # if sensor is not used, return empty sensor data
+ print("not options.use_sensor: returns None ->", options.use_sensor)
+ sensor_data = None
+ elif (sensor.record is None) and (not options.cuboid_corners):
+ # if sensor.record is not given by the user, reassign sensor_data.p to sensor_data
+ print("reassigns. Not sure if there is a check for whether this exists though")
+ sensor_data = sensor_data.p
+ else:
+ pass
+
+ # update command line status
+ t_total = t0 + t1
+ print('\ttotal computation time', scale_time(t_total), '\n')
+
+ return sensor_data
diff --git a/kwave/reconstruction/beamform.py b/kwave/reconstruction/beamform.py
index 2f089b6f1..6b80308f6 100644
--- a/kwave/reconstruction/beamform.py
+++ b/kwave/reconstruction/beamform.py
@@ -41,28 +41,46 @@ def focus(kgrid, input_signal, source_mask, focus_position, sound_speed):
# filter_positions
positions = [position for position in positions if (position != np.nan).any()]
- assert len(positions) == kgrid.dim
+ assert len(positions) == kgrid.dim, "positions have wrong dimensions"
positions = np.array(positions)
if isinstance(focus_position, list):
focus_position = np.array(focus_position)
- assert isinstance(focus_position, np.ndarray)
+ assert isinstance(focus_position, np.ndarray), "focus_position is not an np.array"
dist = np.linalg.norm(positions[:, source_mask.flatten() == 1] - focus_position[:, np.newaxis])
- # distance to delays
- delay = int(np.round(dist / (kgrid.dt * sound_speed)))
- max_delay = np.max(delay)
- rel_delay = -(delay - max_delay)
+ # calculate the distance from every point in the source mask to the focus position
+ if kgrid.dim == 1:
+ dist = np.abs(kgrid.x[source_mask == 1] - focus_position[0])
+ elif kgrid.dim == 2:
+ dist = np.sqrt((kgrid.x[source_mask == 1] - focus_position[0])**2 +
+ (kgrid.y[source_mask == 1] - focus_position[1])**2 )
+ elif kgrid.dim == 3:
+ dist = np.sqrt((kgrid.x[source_mask == 1] - focus_position[0])**2 +
+ (kgrid.y[source_mask == 1] - focus_position[1])**2 +
+ (kgrid.z[source_mask == 1] - focus_position[2])**2 )
- signal_mat = np.zeros((rel_delay.size, input_signal.size + max_delay))
+ # convert distances to time delays
+ delays = np.round(dist / (kgrid.dt * sound_speed)).astype(int)
- # for src_idx, delay in enumerate(rel_delay):
- # signal_mat[src_idx, delay:max_delay - delay] = input_signal
- # signal_mat[rel_delay, delay:max_delay - delay] = input_signal
+ # convert time points to delays relative to the maximum delays
+ relative_delays = delays.max() - delays
+
+ # largest time delay
+ max_delay = np.max(relative_delays)
+
+ # allocate array
+ signal_mat = np.zeros((relative_delays.size, input_signal.size + max_delay), order='F')
+
+ # assign the input signal
+ for source_index, delay in enumerate(relative_delays):
+ signal_mat[source_index, :] = np.hstack([np.zeros((delay,)),
+ np.squeeze(input_signal),
+ np.zeros((max_delay - delay,))])
logging.log(
- logging.WARN, f"{PendingDeprecationWarning.__name__}: " "This method is not fully migrated, might be depricated and is untested."
+ logging.WARN, f"PendingDeprecationWarning {__name__}: " "This method is not fully migrated, might be depricated and is untested."
)
return signal_mat
diff --git a/kwave/recorder.py b/kwave/recorder.py
index 6f6b27059..79e8dcaf5 100644
--- a/kwave/recorder.py
+++ b/kwave/recorder.py
@@ -5,9 +5,12 @@
from kwave.kgrid import kWaveGrid
-@dataclass
+@dataclass(init=False)
class Recorder(object):
def __init__(self):
+
+ # print("Recorder initialized")
+
# flags which control which parameters are recorded
self.p = True #: time-varying acoustic pressure
self.p_max = False #: maximum pressure over simulation
@@ -34,6 +37,9 @@ def __init__(self):
self.y1_inside, self.y2_inside = None, None
self.z1_inside, self.z2_inside = None, None
+ # print("self.p:", self.p)
+
+
def set_flags_from_list(self, flags_list: List[str], is_elastic_code: bool) -> None:
"""
Set Recorder flags that are present in the string list to True
@@ -48,17 +54,19 @@ def set_flags_from_list(self, flags_list: List[str], is_elastic_code: bool) -> N
# check the contents of the cell array are valid inputs
allowed_flags = self.get_allowed_flags(is_elastic_code)
for record_element in flags_list:
- assert record_element in allowed_flags, f"{record_element} is not a valid input for sensor.record"
+ assert record_element in allowed_flags, f"{record_element} is not a valid input for recording"
if record_element == "p": # custom logic for 'p'
continue
else:
+ # print(record_element)
setattr(self, record_element, True)
# set self.record_p to false if a user input for sensor.record
# is given and 'p' is not set (default is true)
self.p = "p" in flags_list
+
def set_index_variables(self, kgrid: kWaveGrid, pml_size: Vector, is_pml_inside: bool, is_axisymmetric: bool) -> None:
"""
Assign the index variables
@@ -73,28 +81,31 @@ def set_index_variables(self, kgrid: kWaveGrid, pml_size: Vector, is_pml_inside:
None
"""
if not is_pml_inside:
- self.x1_inside = pml_size.x + 1.0
- self.x2_inside = kgrid.Nx - pml_size.x
+ self.x1_inside: int = pml_size.x + 1
+ self.x2_inside: int = kgrid.Nx - pml_size.x
if kgrid.dim == 2:
if is_axisymmetric:
- self.y1_inside = 1
+ self.y1_inside: int = 1
else:
- self.y1_inside = pml_size.y + 1.0
- self.y2_inside = kgrid.Ny - pml_size.y
+ self.y1_inside: int = pml_size.y + 1
+ self.y2_inside: int = kgrid.Ny - pml_size.y
elif kgrid.dim == 3:
- self.y1_inside = pml_size.y + 1.0
- self.y2_inside = kgrid.Ny - pml_size.y
- self.z1_inside = pml_size.z + 1.0
- self.z2_inside = kgrid.Nz - pml_size.z
+ self.y1_inside: int = pml_size.y + 1
+ self.y2_inside: int = kgrid.Ny - pml_size.y
+ self.z1_inside: int = pml_size.z + 1
+ self.z2_inside: int = kgrid.Nz - pml_size.z
else:
- self.x1_inside = 1.0
- self.x2_inside = kgrid.Nx
+ self.x1_inside: int = 1
+ self.x2_inside: int = kgrid.Nx
if kgrid.dim == 2:
- self.y1_inside = 1.0
- self.y2_inside = kgrid.Ny
+ self.y1_inside: int = 1
+ self.y2_inside: int = kgrid.Ny
if kgrid.dim == 3:
- self.z1_inside = 1.0
- self.z2_inside = kgrid.Nz
+ self.y1_inside: int = 1
+ self.y2_inside: int = kgrid.Ny
+ self.z1_inside: int = 1
+ self.z2_inside: int = kgrid.Nz
+
@staticmethod
def get_allowed_flags(is_elastic_code):
@@ -102,7 +113,7 @@ def get_allowed_flags(is_elastic_code):
Get the list of allowed flags for a given simulation type
Args:
- is_elastic_code: Whether the simulation is axisymmetric
+ is_elastic_code: Whether the simulation is elastic
Returns:
List of allowed flags for a given simulation type
diff --git a/kwave/utils/conversion.py b/kwave/utils/conversion.py
index a1486ca82..887e53a4e 100644
--- a/kwave/utils/conversion.py
+++ b/kwave/utils/conversion.py
@@ -30,7 +30,7 @@ def db2neper(alpha: Real[kt.ArrayLike, "..."], y: Real[kt.ScalarLike, ""] = 1) -
"""
# calculate conversion
- alpha = 100.0 * alpha * (1e-6 / (2.0 * math.pi)) ** y / (20.0 * np.log10(np.exp(1)))
+ alpha = 100.0 * alpha * (1e-6 / (2.0 * math.pi)) ** y / (20.0 * np.log10(np.exp(1.0)))
return alpha
diff --git a/kwave/utils/filters.py b/kwave/utils/filters.py
index 50aab667c..146078666 100644
--- a/kwave/utils/filters.py
+++ b/kwave/utils/filters.py
@@ -120,7 +120,9 @@ def spect(
# window the signal, reshaping the window to be in the correct direction
win, coherent_gain = get_win(func_length, window, symmetric=False)
+
win = np.reshape(win, tuple(([1] * dim + [func_length] + [1] * (len(sz) - 2))))
+
func = win * func
# compute the fft using the defined FFT length, if fft_len >
@@ -410,7 +412,7 @@ def filter_time_series(
signal: np.ndarray,
ppw: Optional[int] = 3,
rppw: Optional[int] = 0,
- stop_band_atten: Optional[int] = 60,
+ stop_band_atten: Optional[float] = 60.0,
transition_width: Optional[float] = 0.1,
zerophase: Optional[bool] = False,
plot_spectrums: Optional[bool] = False,
@@ -459,14 +461,29 @@ def filter_time_series(
"""
- # check the input is a row vector
- if num_dim2(signal) == 1:
+ rotate_signal = False
+ if np.ndim(signal) == 2:
m, n = signal.shape
- if n == 1:
+ if n == 1 and m != 1:
signal = signal.T
rotate_signal = True
- else:
+ elif m == 1 and n != 1:
rotate_signal = False
+ else:
+ TypeError("Input signal must be a vector.")
+
+ signal = np.expand_dims(np.squeeze(signal), axis=-1)
+
+ # check the input is a row vector
+ if num_dim2(signal) == 1:
+ # print(np.shape(signal), num_dim2(signal))
+ # m, n = signal.shape
+ # if n == 1:
+ # signal = signal.T
+ # rotate_signal = True
+ # else:
+ # rotate_signal = False
+ pass
else:
raise TypeError("Input signal must be a vector.")
@@ -477,58 +494,48 @@ def filter_time_series(
assert not isinstance(kgrid.t_array, str) or kgrid.t_array != "auto", "kgrid.t_array must be explicitly defined."
# compute the sampling frequency
- Fs = 1 / kgrid.dt
+ Fs = 1.0 / kgrid.dt
# extract the minium sound speed
- if medium.sound_speed is not None:
- # for the fluid code, use medium.sound_speed
- c0 = medium.sound_speed.min()
-
- elif all(medium.is_defined("sound_speed_compression", "sound_speed_shear")): # pragma: no cover
+ if all(medium.is_defined("sound_speed_compression", "sound_speed_shear")):
# for the elastic code, combine the shear and compression sound speeds and remove zeros values
ss = np.hstack([medium.sound_speed_compression, medium.sound_speed_shear])
ss[ss == 0] = np.nan
c0 = np.nanmin(ss)
-
# cleanup unused variables
del ss
-
else:
- raise ValueError(
- "The input fields medium.sound_speed or medium.sound_speed_compression and medium.sound_speed_shear must " "be defined."
- )
+ c0 = medium.sound_speed.min()
# extract the maximum supported frequency (two points per wavelength)
- f_max = kgrid.k_max_all * c0 / (2 * np.pi)
+ f_max = kgrid.k_max_all * c0 / (2.0 * np.pi)
# calculate the filter cut-off frequency
- filter_cutoff_f = 2 * f_max / ppw
+ filter_cutoff_f = 2.0 * f_max / ppw
# calculate the wavelength of the filter cut-off frequency as a number of time steps
- filter_wavelength = (2 * np.pi / filter_cutoff_f) / kgrid.dt
+ filter_wavelength = (2.0 * np.pi / filter_cutoff_f) / kgrid.dt
# filter the signal if required
if ppw != 0:
filtered_signal = apply_filter(
- signal,
- Fs,
- float(filter_cutoff_f),
- "LowPass",
+ signal=signal,
+ Fs=Fs,
+ cutoff_f=float(filter_cutoff_f),
+ filter_type="LowPass",
zero_phase=zerophase,
stop_band_atten=float(stop_band_atten),
transition_width=transition_width,
)
- # add a start-up ramp if required
+ # add a start-upp ramp if required
if rppw != 0:
# calculate the length of the ramp in time steps
- ramp_length = round(rppw * filter_wavelength / (2 * ppw))
-
+ ramp_length = round(rppw * filter_wavelength / (2.0 * ppw))
# create the ramp
- ramp = (-np.cos(np.arange(0, ramp_length - 1 + 1) * np.pi / ramp_length) + 1) / 2
-
+ ramp = (-np.cos(np.arange(0, ramp_length) * np.pi / ramp_length) + 1.0) / 2.0
# apply the ramp
- filtered_signal[1:ramp_length] = filtered_signal[1:ramp_length] * ramp
+ filtered_signal[0:ramp_length] = filtered_signal[0:ramp_length] * ramp
# restore the original vector orientation if modified
if rotate_signal:
@@ -558,7 +565,8 @@ def apply_filter(
filter_type: str,
zero_phase: Optional[bool] = False,
transition_width: Optional[float] = 0.1,
- stop_band_atten: Optional[int] = 60,
+ window: Optional[np.ndarray] = None,
+ stop_band_atten: Optional[float] = 60,
) -> np.ndarray:
"""
Filters an input signal using a FIR filter with Kaiser window coefficients based on the specified cut-off frequency and filter type.
@@ -585,7 +593,13 @@ def apply_filter(
# apply the low pass filter
func_filt_lp = apply_filter(
- signal, Fs, cutoff_f[1], "LowPass", stop_band_atten=stop_band_atten, transition_width=transition_width, zero_phase=zero_phase
+ signal,
+ Fs,
+ cutoff_f[1],
+ "LowPass",
+ stop_band_atten=stop_band_atten,
+ transition_width=transition_width,
+ zero_phase=zero_phase,
)
# apply the high pass filter
@@ -605,7 +619,7 @@ def apply_filter(
high_pass = False
elif filter_type == "HighPass":
high_pass = True
- cutoff_f = Fs / 2 - cutoff_f
+ cutoff_f = Fs / 2.0 - cutoff_f
else:
raise ValueError(f'Unknown filter type {filter_type}. Options are "LowPass, HighPass, BandPass"')
@@ -616,31 +630,29 @@ def apply_filter(
# correct the stopband attenuation if a zero phase filter is being used
if zero_phase:
- stop_band_atten = stop_band_atten / 2
+ stop_band_atten = stop_band_atten / 2.0
# decide the filter order
- N = np.ceil((stop_band_atten - 7.95) / (2.285 * (transition_width * np.pi)))
- N = int(N)
+ N = np.ceil((stop_band_atten - 7.95) / (2.285 * (transition_width * np.pi))).astype(int)
# construct impulse response of ideal bandpass filter h(n), a sinc function
fc = cutoff_f / Fs # normalised cut-off
- n = np.arange(-N / 2, N / 2)
- h = 2 * fc * sinc(2 * np.pi * fc * n)
+ n = np.arange(-N / 2.0, N / 2.0)
+ h = 2.0 * fc * sinc(2.0 * np.pi * fc * n)
# if no window is given, use a Kaiser window
- # TODO: there is no window argument
- if "w" not in locals():
+ if window is None:
# compute Kaiser window parameter beta
if stop_band_atten > 50:
beta = 0.1102 * (stop_band_atten - 8.7)
elif stop_band_atten >= 21:
- beta = 0.5842 * (stop_band_atten - 21) ** 0.4 + 0.07886 * (stop_band_atten - 21)
+ beta = 0.5842 * (stop_band_atten - 21.0) ** 0.4 + 0.07886 * (stop_band_atten - 21.0)
else:
- beta = 0
+ beta = 0.0
# construct the Kaiser smoothing window w(n)
m = np.arange(0, N)
- w = np.real(scipy.special.iv(0, np.pi * beta * np.sqrt(1 - (2 * m / N - 1) ** 2))) / np.real(scipy.special.iv(0, np.pi * beta))
+ w = np.real(scipy.special.i0(np.pi * beta * np.sqrt(1.0 - (2.0 * m / N - 1.0) ** 2))) / np.real(scipy.special.i0(np.pi * beta))
# window the ideal impulse response with Kaiser window to obtain the FIR filter coefficients hw(n)
hw = w * h
@@ -654,12 +666,12 @@ def apply_filter(
filtered_signal = np.hstack([np.zeros((1, N)), signal]).squeeze()
# apply the filter
- filtered_signal = lfilter(hw.squeeze(), 1, filtered_signal)
+ filtered_signal = lfilter(hw.squeeze(), 1.0, filtered_signal)
if zero_phase:
filtered_signal = np.fliplr(lfilter(hw.squeeze(), 1, filtered_signal[np.arange(L + N, 1, -1)]))
# remove the part of the signal corresponding to the added zeros
- filtered_signal = filtered_signal[N:]
+ filtered_signal = filtered_signal[N:(L+N+1)]
return filtered_signal[np.newaxis]
@@ -690,10 +702,15 @@ def smooth(a: np.ndarray, restore_max: Optional[bool] = False, window_type: Opti
# get the grid size
grid_size = a.shape
+ # print("[in smooth:grid_size]", grid_size)
+
# remove singleton dimensions
if num_dim2(a) != len(grid_size):
grid_size = np.squeeze(grid_size)
+ if a.ndim == 1:
+ a = a.reshape((1, -1))
+
# use a symmetric filter for odd grid sizes, and a non-symmetric filter for
# even grid sizes to ensure the DC component of the window has a value of
# unity
@@ -710,9 +727,13 @@ def smooth(a: np.ndarray, restore_max: Optional[bool] = False, window_type: Opti
if a.shape[0] == 1: # is row?
win = win.T
+ # print("**************", a.shape, win.shape)
+
# apply the filter
a_sm = np.real(np.fft.ifftn(np.fft.fftn(a) * np.fft.ifftshift(win)))
+ # print("**************", a_sm.shape, win.shape)
+
# restore magnitude if required
if restore_max:
a_sm = (np.abs(a).max() / np.abs(a_sm).max()) * a_sm
diff --git a/kwave/utils/kwave_array.py b/kwave/utils/kwave_array.py
index 9996e055a..15e0561e4 100644
--- a/kwave/utils/kwave_array.py
+++ b/kwave/utils/kwave_array.py
@@ -84,6 +84,8 @@ def __post_init__(self):
self.measure = float(self.measure)
+
+
class kWaveArray(object):
def __init__(
self,
diff --git a/kwave/utils/mapgen.py b/kwave/utils/mapgen.py
index 3c957be96..44e534cfa 100644
--- a/kwave/utils/mapgen.py
+++ b/kwave/utils/mapgen.py
@@ -1022,6 +1022,7 @@ def make_line(
xx = np.array([a[0], b[0]], dtype=int)
yy = np.array([a[1], b[1]], dtype=int)
if np.any(a < 0) or np.any(b < 0) or np.any(xx > grid_size.x - 1) or np.any(yy > grid_size.y - 1):
+ print("a,b", a, b)
raise ValueError("Both the start and end points must lie within the grid.")
if linetype == "angled":
@@ -1554,11 +1555,11 @@ def make_pixel_map_plane(grid_size: Vector, normal: np.ndarray, point: np.ndarra
return pixel_map
-@typechecker
+# @typechecker
def make_bowl(
grid_size: Vector,
bowl_pos: Vector,
- radius: Union[int, float],
+ radius: Union[int, float, Int[kt.ScalarLike, ""]],
diameter: Real[kt.ScalarLike, ""],
focus_pos: Vector,
binary: bool = False,
@@ -1632,16 +1633,22 @@ def make_bowl(
# BOUND THE GRID TO SPEED UP CALCULATION
# =========================================================================
+ if isinstance(diameter, np.ndarray):
+ if np.size(diameter == 1):
+ diameter = diameter.item()
+
# create bounding box slightly larger than bowl diameter * sqrt(2)
Nx = np.round(np.sqrt(2) * diameter).astype(int) + BOUNDING_BOX_EXP
Ny = Nx
Nz = Nx
grid_size_sm = Vector([Nx, Ny, Nz])
+ # print("sizes in bowl:", Nx, Ny, Nz)
+
# set the bowl position to be the centre of the bounding box
- bx = np.ceil(Nx / 2).astype(int)
- by = np.ceil(Ny / 2).astype(int)
- bz = np.ceil(Nz / 2).astype(int)
+ bx = np.ceil(Nx // 2 - 1).astype(int)
+ by = np.ceil(Ny // 2 - 1).astype(int)
+ bz = np.ceil(Nz // 2 - 1).astype(int)
bowl_pos_sm = np.array([bx, by, bz])
# set the focus position to be in the direction specified by the user
@@ -1666,9 +1673,9 @@ def make_bowl(
# find centre of sphere on which the bowl lies
distance_cf = np.sqrt((bx - fx) ** 2 + (by - fy) ** 2 + (bz - fz) ** 2)
- cx = round(radius / distance_cf * (fx - bx) + bx)
- cy = round(radius / distance_cf * (fy - by) + by)
- cz = round(radius / distance_cf * (fz - bz) + bz)
+ cx = np.round(radius / distance_cf * (fx - bx) + bx).astype(int)
+ cy = np.round(radius / distance_cf * (fy - by) + by).astype(int)
+ cz = np.round(radius / distance_cf * (fz - bz) + bz).astype(int)
c = np.array([cx, cy, cz])
# generate matrix with distance from the centre
@@ -1687,6 +1694,16 @@ def make_bowl(
# calculate distance from search radius
pixel_map = np.abs(pixel_map - search_radius)
+ # offset: int = 1
+
+ # x_foward_shift: int = 1
+ # y_foward_shift: int = 1
+ # z_foward_shift: int = 1
+
+ # x_backwards_shift: int = 1
+ # y_backwards_shift: int = 1
+ # z_backwards_shift: int = 1
+
# =========================================================================
# DIMENSION 1
# =========================================================================
@@ -1830,6 +1847,7 @@ def make_bowl(
# if the angle is greater than the half angle of the bowl, remove
# it from the bowl
+ # print(l2, l1, v1, v2, np.shape(v1 * v2), theta, half_arc_angle)
if theta > half_arc_angle:
bowl_sm = matlab_assign(bowl_sm, bowl_ind_i - 1, 0)
@@ -2090,41 +2108,92 @@ def make_bowl(
z1 = bowl_pos[2] - bz
z2 = z1 + Nz
+ print("MultiBowl information: pre")
+ print(bx,by,bz,bowl_pos, Nx, Ny, Nz)
+ print(x1,x2,y1,y2,z1,z2)
+
# truncate bounding box if it falls outside the grid
if x1 < 0:
- bowl_sm = bowl_sm[abs(x1) :, :, :]
+ #bowl_sm = bowl_sm[abs(x1):, :, :]
+
+ s = slice(0, abs(x1))
+ bowl_sm = np.delete(bowl_sm, s, axis=0)
x1 = 0
+
+ # print("x1 < 0", bowl_pos[0], bx, x1, s.start, s.stop, s.step,
+ # np.shape(bowl_sm), Nx, Ny, Nz, grid_size)
+ # print("x1 < 0", np.shape(bowl_sm))
+
+ # x1 = x1 + 1
+ # # x2 = x2 + 1
+ # print("x1 < 0", np.shape(bowl_sm))
+
if y1 < 0:
- bowl_sm = bowl_sm[:, abs(y1) :, :]
+ # bowl_sm = bowl_sm[:, abs(y1):, :]
+ bowl_sm = np.delete(bowl_sm, slice(0, abs(y1)), axis=1)
y1 = 0
+ # print("y1 < 0", np.shape(bowl_sm))
if z1 < 0:
- bowl_sm = bowl_sm[:, :, abs(z1) :]
+ # bowl_sm = bowl_sm[:, :, abs(z1):]
+ bowl_sm = np.delete(bowl_sm, slice(0, abs(z1)), axis=2)
z1 = 0
- if x2 >= grid_size[0]:
- to_delete = x2 - grid_size[0]
- bowl_sm = bowl_sm[:-to_delete, :, :]
+ # print("z1 < 0", np.shape(bowl_sm))
+ if x2 > grid_size[0]-1:
+ # to_delete = x2 - grid_size[0]
+ # bowl_sm = bowl_sm[:-to_delete, :, :]
+ start_index = bowl_sm.shape[0] - (x2 - grid_size[0])
+ end_index = bowl_sm.shape[0]
+ s = slice(start_index, end_index)
+ bowl_sm = np.delete(bowl_sm, slice(start_index, end_index), axis=0)
x2 = grid_size[0]
- if y2 >= grid_size[1]:
- to_delete = y2 - grid_size[1]
- bowl_sm = bowl_sm[:, :-to_delete, :]
+ # print("x2 > Nx", np.shape(bowl_sm))
+ # print("x2 < Nx", bowl_pos[0], bx, x2, s.start, s.stop, s.step, np.shape(bowl_sm))
+
+ if y2 > grid_size[1] - 1:
+ # to_delete = y2 - grid_size[1]
+ # bowl_sm = bowl_sm[:, :-to_delete, :]
+ start_index = bowl_sm.shape[1] - (y2 - grid_size[1])
+ end_index = bowl_sm.shape[1]
+ bowl_sm = np.delete(bowl_sm, slice(start_index, end_index), axis=1)
y2 = grid_size[1]
- if z2 >= grid_size[2]:
- to_delete = z2 - grid_size[2]
- bowl_sm = bowl_sm[:, :, :-to_delete]
+ # print("y2 > Ny", np.shape(bowl_sm))
+ if z2 > grid_size[2] - 1:
+ # to_delete = z2 - grid_size[2]
+ # bowl_sm = bowl_sm[:, :, :-to_delete]
+ start_index = bowl_sm.shape[2] - (z2 - grid_size[2])
+ end_index = bowl_sm.shape[2]
+ bowl_sm = np.delete(bowl_sm, slice(start_index, end_index), axis=2)
z2 = grid_size[2]
+ # print("z2 > Nz", np.shape(bowl_sm))
+
+ # x1 = x1 + 1
+ # x2 = x2 + 1
+ # y1 = y1 + 1
+ # y2 = y2 + 1
+ # z1 = z1 + 1
+ # z2 = z2 + 1
+
+ print("MultiBowl information: post")
+ print(x1,x2,y1,y2,z1,z2)
+ print(np.shape(bowl), np.shape(bowl_sm), np.shape(bowl[x1:x2, y1:y2, z1:z2]), grid_size)
+
+ # shifted_mask[1:, 1:, 1:] = binary_mask[:-1, :-1, :-1]
+ # print(np.shape(bowl[x1:x2, y1:y2, z1:z2]), np.shape(bowl_sm))
- # place bowl into grid
bowl[x1:x2, y1:y2, z1:z2] = bowl_sm
+ # shifted_bowl = np.zeros_like(bowl)
+ # shifted_bowl[1:, 1:, 1:] = bowl[:-1, :-1, :-1]
+
return bowl
def make_multi_bowl(
grid_size: Vector,
- bowl_pos: List[Tuple[int, int]],
- radius: int,
- diameter: int,
- focus_pos: Tuple[int, int],
+ bowl_pos: List[Tuple[int, int, int]],
+ radius: List[int],
+ diameter: List[int],
+ focus_pos: List[Tuple[int, int, int]],
binary: bool = False,
remove_overlap: bool = False,
) -> Tuple[np.ndarray, List[np.ndarray]]:
@@ -2134,10 +2203,10 @@ def make_multi_bowl(
Args:
grid_size: The size of the grid (assumed to be square).
- bowl_pos: A list of tuples containing the (x, y) coordinates of the center of each bowl.
+ bowl_pos: A list of tuples containing the (x, y, z) coordinates of the center of each bowl.
radius: The radius of each bowl.
diameter: The diameter of the bowls.
- focus_pos: The (x, y) coordinates of the focus.
+ focus_pos: The list of (x, y, z) coordinates of the focus.
binary: Whether to return a binary mask (default: False).
remove_overlap: Whether to remove overlap between the bowls (default: False).
@@ -2147,7 +2216,7 @@ def make_multi_bowl(
"""
# check inputs
- if bowl_pos.shape[-1] != 3:
+ if np.shape(np.asarray(bowl_pos))[1] != 3:
raise ValueError("bowl_pos should contain 3 columns, with [bx, by, bz] in each row.")
if len(radius) != 1 and len(radius) != bowl_pos.shape[0]:
@@ -2189,32 +2258,33 @@ def make_multi_bowl(
bowl_pos_k = bowl_pos[bowl_index]
else:
bowl_pos_k = bowl_pos
- bowl_pos_k = Vector(bowl_pos_k)
+ # bowl_pos_k = Vector(bowl_pos_k)
if len(radius) > 1:
radius_k = radius[bowl_index]
else:
- radius_k = radius
+ radius_k = radius.item()
if len(diameter) > 1:
diameter_k = diameter[bowl_index]
else:
- diameter_k = diameter
+ diameter_k = diameter[0].item()
if focus_pos.shape[0] > 1:
focus_pos_k = focus_pos[bowl_index]
else:
- focus_pos_k = focus_pos
+ focus_pos_k = focus_pos[0]
focus_pos_k = Vector(focus_pos_k)
# create new bowl
- new_bowl = make_bowl(grid_size, bowl_pos_k, radius_k, diameter_k, focus_pos_k, remove_overlap=remove_overlap, binary=binary)
+ new_bowl = make_bowl(grid_size, bowl_pos_k, radius_k, diameter_k, focus_pos_k,
+ remove_overlap=remove_overlap, binary=binary)
# add bowl to bowl matrix
bowls = bowls + new_bowl
# add new bowl to labelling matrix
- bowls_labelled[new_bowl == 1] = bowl_index
+ bowls_labelled[new_bowl == 1] = bowl_index + int(1)
TicToc.toc()
@@ -2341,7 +2411,7 @@ def make_sphere(
"""
assert len(grid_size) == 3, "grid_size must be a 3D vector"
- # enforce a centered sphere
+ # enforce a centered sphere: matlab indexed
center = np.floor(grid_size / 2).astype(int) + 1
# preallocate the storage variable
@@ -2471,6 +2541,8 @@ def make_spherical_section(
# flatten transducer and store the maximum and indices
mx = np.squeeze(np.max(ss, axis=0))
+ print(np.shape(mx))
+
# calculate the total length/width of the transducer
length = mx[(len(mx) + 1) // 2].sum()
diff --git a/kwave/utils/matlab.py b/kwave/utils/matlab.py
index a589380c4..b59299d92 100644
--- a/kwave/utils/matlab.py
+++ b/kwave/utils/matlab.py
@@ -2,7 +2,6 @@
import numpy as np
-
def rem(x, y, rtol=1e-05, atol=1e-08):
"""
Returns the remainder after division of x by y, taking into account the floating point precision.
@@ -75,7 +74,7 @@ def matlab_find(arr: Union[List[int], np.ndarray], val: int = 0, mode: str = "ne
return np.expand_dims(arr, -1) # compatibility, n => [n, 1]
-def matlab_mask(arr: np.ndarray, mask: np.ndarray, diff: Optional[int] = None) -> np.ndarray:
+def matlab_mask(arr: np.ndarray, mask: np.ndarray, diff = None) -> np.ndarray:
"""
Applies a mask to an array and returns the masked elements.
@@ -95,7 +94,7 @@ def matlab_mask(arr: np.ndarray, mask: np.ndarray, diff: Optional[int] = None) -
return np.expand_dims(arr.ravel(order="F")[mask.ravel(order="F") + diff], axis=-1) # compatibility, n => [n, 1]
-def unflatten_matlab_mask(arr: np.ndarray, mask: np.ndarray, diff: Optional[int] = None) -> Tuple[Union[int, np.ndarray], ...]:
+def unflatten_matlab_mask(arr: np.ndarray, mask: np.ndarray, diff = None) -> Tuple[Union[int, np.ndarray], ...]:
"""
Converts a mask array to a tuple of subscript indices for an n-dimensional array.
diff --git a/kwave/utils/matrix.py b/kwave/utils/matrix.py
index 73671c74f..8c2b3422d 100644
--- a/kwave/utils/matrix.py
+++ b/kwave/utils/matrix.py
@@ -93,7 +93,7 @@ def trim_zeros(data: Num[np.ndarray, "..."]) -> Tuple[Num[np.ndarray, "..."], Li
def expand_matrix(
matrix: Union[Num[np.ndarray, "..."], Bool[np.ndarray, "..."]],
exp_coeff: Union[Shaped[kt.ArrayLike, "dim"], List],
- edge_val: Optional[Real[kt.ScalarLike, ""]] = None,
+ edge_val: Optional[Real[kt.ScalarLike, ""]] = None, verbose: bool = False
):
"""
Enlarge a matrix by extending the edge values.
@@ -134,7 +134,10 @@ def expand_matrix(
exp_coeff = np.array(exp_coeff).astype(int).squeeze()
n_coeff = exp_coeff.size
- assert n_coeff > 0
+ assert n_coeff > 0, "exp_coeff must be well-defined"
+
+ if verbose:
+ print(n_coeff, len(matrix.shape), exp_coeff)
if n_coeff == 1:
opts["pad_width"] = exp_coeff
@@ -152,7 +155,19 @@ def expand_matrix(
if n_coeff == 6:
opts["pad_width"] = [(exp_coeff[0], exp_coeff[1]), (exp_coeff[2], exp_coeff[3]), (exp_coeff[4], exp_coeff[5])]
- return np.pad(matrix, **opts)
+ if verbose:
+ print("\toptions:", opts, opts["pad_width"] )
+ print("\tmatrix shape:", np.shape(matrix))
+
+ if verbose:
+ new_matrix = np.pad(matrix, pad_width=[30, 2], mode='constant', constant_values=0.0)
+ else:
+ new_matrix = np.pad(matrix, **opts)
+
+ if verbose:
+ print("\tnew matrix shape", np.shape(new_matrix))
+
+ return new_matrix
def resize(mat: np.ndarray, new_size: Union[int, List[int]], interp_mode: str = "linear") -> np.ndarray:
diff --git a/kwave/utils/signals.py b/kwave/utils/signals.py
index e420f3f2a..7e2329575 100644
--- a/kwave/utils/signals.py
+++ b/kwave/utils/signals.py
@@ -57,7 +57,7 @@ def add_noise(signal: np.ndarray, snr: float, mode="rms"):
@typechecker
def get_win(
- N: Union[int, np.ndarray, Tuple[int, int], Tuple[int, int, int], List[Int[kt.ScalarLike, ""]]],
+ N: Union[int, np.ndarray, Tuple[int,], Tuple[int, int], Tuple[int, int, int], List[Int[kt.ScalarLike, ""]]],
# TODO: replace and refactor for scipy.signal.get_window
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.get_window.html#scipy.signal.get_window
type_: str, # TODO change this to enum in the future
@@ -215,7 +215,7 @@ def cosine_series(n: int, N: int, coeffs: List[float]) -> np.ndarray:
# trim the window if required
if not symmetric:
N -= 1
- win = win[0:N]
+ win = win[0:int(N)]
win = np.expand_dims(win, axis=-1)
# calculate the coherent gain
@@ -474,6 +474,7 @@ def reorder_sensor_data(kgrid, sensor, sensor_data: np.ndarray) -> np.ndarray:
# reorder the measure time series so that adjacent time series correspond
# to adjacent sensor points.
reordered_sensor_data = sensor_data[indices_new]
+
return reordered_sensor_data
@@ -488,7 +489,7 @@ def reorder_binary_sensor_data(sensor_data: np.ndarray, reorder_index: np.ndarra
"""
reorder_index = np.squeeze(reorder_index)
- assert sensor_data.ndim == 2
+ assert sensor_data.ndim == 2, "sensor_data has dimensions: " + str(sensor_data.ndim)
assert reorder_index.ndim == 1
return sensor_data[reorder_index.argsort()]
diff --git a/pyproject.toml b/pyproject.toml
index 605b51770..fae13455b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version"]
description = "Acoustics toolbox for time domain acoustic and ultrasound simulations in complex and tissue-realistic media."
readme = "docs/README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.9"
+requires-python = ">=3.10"
authors = [
{ name = "Farid Yagubbayli", email = "farid.yagubbayli@tum.de" },
{ name = "Walter Simson", email = "walter.simson@tum.de"}
@@ -24,14 +24,15 @@ classifiers = [
"Programming Language :: Python :: 3",
]
dependencies = [
- "h5py==3.12.1",
- "scipy==1.13.1",
- "opencv-python==4.10.0.84",
- "deepdiff==8.0.1",
- "matplotlib==3.9.2",
- "numpy>=1.22.2,<1.27.0",
- "beartype==0.19.0",
- "jaxtyping==0.2.34"
+ "h5py==3.13.0",
+ "scipy==1.15.2",
+ "opencv-python==4.11.0.86",
+ "deepdiff==8.5.0",
+ "numpy>=1.22.2,<2.3.0",
+ "matplotlib==3.10.3",
+ "beartype==0.20.2",
+ "jaxtyping==0.3.2",
+ "deprecated>=1.2.14"
]
[project.urls]
@@ -42,17 +43,17 @@ Bug-tracker = "https://github.com/waltsims/k-wave-python/issues"
[project.optional-dependencies]
test = ["pytest",
- "coverage==7.6.3",
+ "coverage==7.8.0",
"phantominator",
"testfixtures==8.3.0",
"requests==2.32.3"]
example = ["gdown==5.2.0"]
docs = [ "sphinx-mdinclude==0.6.2",
"sphinx-copybutton==0.5.2",
- "sphinx-tabs==3.4.5",
+ "sphinx-tabs==3.4.7",
"sphinx-toolbox==3.8.0",
"furo==2024.8.6"]
-dev = ["pre-commit==4.0.1"]
+dev = ["pre-commit==4.2.0"]
[tool.hatch.version]
path = "kwave/__init__.py"
@@ -80,8 +81,11 @@ exclude = [
testpaths = ["tests"]
filterwarnings = [
"error::DeprecationWarning",
- "error::PendingDeprecationWarning"
+ "error::PendingDeprecationWarning",
+ "ignore::deprecation.DeprecatedWarning",
+ "ignore::DeprecationWarning:kwave"
]
+
[tool.coverage.run]
branch = true
command_line = "-m pytest"
@@ -98,6 +102,13 @@ omit = [
line-length = 140
# F821 needed to avoid false-positives in nested functions, F722 due to jaxtyping
lint.ignore = ["F821", "F722"]
+lint.select = ["NPY201", "I"]
+
+# Configure isort rules
+[tool.ruff.lint.isort]
+known-first-party = ["kwave", "examples"]
+section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
+
[tool.ruff.lint.per-file-ignores]
# ksource.py contains a lot of non-ported Matlab code that is not usable.
"kwave/ksource.py" = ["F821"]
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 000000000..ff6dbad89
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+filterwarnings =
+ ignore:The interactive_bk attribute was deprecated.*:matplotlib._api.deprecation.MatplotlibDeprecationWarning
+ # Add any other warning filters you might need below
\ No newline at end of file
diff --git a/tests/test_pstd_elastic_2d_check_split_field.py b/tests/test_pstd_elastic_2d_check_split_field.py
new file mode 100644
index 000000000..5c3dde47e
--- /dev/null
+++ b/tests/test_pstd_elastic_2d_check_split_field.py
@@ -0,0 +1,156 @@
+
+
+"""
+Unit test to check that the split field components sum to give the correct field, e.g., ux = ux^p + ux^s.
+"""
+
+import numpy as np
+from copy import deepcopy
+import pytest
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic2D import pstd_elastic_2d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.signals import tone_burst
+from kwave.utils.mapgen import make_arc
+
+@pytest.mark.skip(reason="2D not ready")
+def test_pstd_elastic_2d_check_split_field():
+
+ # set comparison threshold
+ COMPARISON_THRESH = 1e-15
+
+ # set pass variable
+ test_pass = True
+
+ # =========================================================================
+ # SIMULATION PARAMETERS
+ # =========================================================================
+
+ # change scale to 2 to reproduce higher resolution figures in help file
+ scale: int = 1
+
+ # create the computational grid
+ PML_size: int = 10 # [grid points]
+ Nx: int = 128 * scale - 2 * PML_size # [grid points]
+ Ny: int = 192 * scale - 2 * PML_size # [grid points]
+ dx = 0.5e-3 / float(scale) # [m]
+ dy = 0.5e-3 / float(scale) # [m]
+ kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))
+
+ # define the medium properties for the top layer
+ cp1 = 1540 # compressional wave speed [m/s]
+ cs1 = 0 # shear wave speed [m/s]
+ rho1 = 1000 # density [kg/m^3]
+ alpha0_p1 = 0.1 # compressional absorption [dB/(MHz^2 cm)]
+ alpha0_s1 = 0.1 # shear absorption [dB/(MHz^2 cm)]
+
+ # define the medium properties for the bottom layer
+ cp2 = 3000 # compressional wave speed [m/s]
+ cs2 = 1400 # shear wave speed [m/s]
+ rho2 = 1850 # density [kg/m^3]
+ alpha0_p2 = 1 # compressional absorption [dB/(MHz^2 cm)]
+ alpha0_s2 = 1 # shear absorption [dB/(MHz^2 cm)]
+
+ # create the time array
+ cfl = 0.1
+ t_end = 60e-6
+ kgrid.makeTime(cp1, cfl, t_end)
+
+ # define position of heterogeneous slab
+ slab = np.zeros((Nx, Ny))
+ slab[Nx // 2 - 1:, :] = 1
+
+ # define the source geometry in SI units (where 0, 0 is the grid center)
+ arc_pos = [-15e-3, -25e-3] # [m]
+ focus_pos = [5e-3, 5e-3] # [m]
+ radius = 25e-3 # [m]
+ diameter = 20e-3 # [m]
+
+ # define the driving signal
+ source_freq = 500e3 # [Hz]
+ source_strength = 1e6 # [Pa]
+ source_cycles = 3 # number of tone burst cycles
+
+ # define the sensor to record the maximum particle velocity everywhere
+ sensor = kSensor()
+ sensor.record = ['u_split_field', 'u_non_staggered']
+ sensor.mask = np.ones((Nx, Ny))
+
+ # =========================================================================
+ # SIMULATION
+ # =========================================================================
+
+ # convert the source parameters to grid points
+ arc_pos = np.round(np.asarray(arc_pos) / dx).astype(int) + np.asarray([Nx // 2, Ny // 2])
+ focus_pos = np.round(np.asarray(focus_pos) / dx).astype(int) + np.asarray([Nx // 2, Ny // 2])
+ radius = int(round(radius / dx))
+ diameter = int(round(diameter / dx))
+
+ # force the diameter to be odd
+ if diameter % 2 == 0:
+ diameter: int = diameter + int(1)
+
+ # define the medium properties
+ sound_speed_compression = cp1 * np.ones((Nx, Ny))
+ sound_speed_shear = cs1 * np.ones((Nx, Ny))
+ density = rho1 * np.ones((Nx, Ny))
+ alpha_coeff_compression = alpha0_p1 * np.ones((Nx, Ny))
+ alpha_coeff_shear = alpha0_s1 * np.ones((Nx, Ny))
+
+ medium = kWaveMedium(sound_speed_compression,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear,
+ density=density,
+ alpha_coeff_compression=alpha_coeff_compression,
+ alpha_coeff_shear=alpha_coeff_shear)
+
+ medium.sound_speed_compression[slab == 1] = cp2
+ medium.sound_speed_shear[slab == 1] = cs2
+ medium.density[slab == 1] = rho2
+ medium.alpha_coeff_compression[slab == 1] = alpha0_p2
+ medium.alpha_coeff_shear[slab == 1] = alpha0_s2
+
+ # generate the source geometry
+ source_mask = make_arc(Vector([Nx, Ny]), arc_pos, radius, diameter, Vector(focus_pos))
+
+ # assign the source
+ source = kSource()
+ source.s_mask = source_mask
+ fs = 1.0 / kgrid.dt
+ source.sxx = -source_strength * tone_burst(fs, source_freq, source_cycles)
+ source.syy = source.sxx
+
+ simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=False,
+ pml_size=PML_size)
+
+ # run the elastic simulation
+ sensor_data_elastic = pstd_elastic_2d(deepcopy(kgrid),
+ medium=deepcopy(medium),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ simulation_options=deepcopy(simulation_options))
+
+ # compute errors
+ diff_ux = np.max(np.abs(sensor_data_elastic['ux_non_staggered'] -
+ sensor_data_elastic['ux_split_p'] -
+ sensor_data_elastic['ux_split_s'])) / np.max(np.abs(sensor_data_elastic['ux_non_staggered']))
+
+ diff_uy = np.max(np.abs(sensor_data_elastic['uy_non_staggered'] -
+ sensor_data_elastic['uy_split_p'] -
+ sensor_data_elastic['uy_split_s'])) / np.max(np.abs(sensor_data_elastic['uy_non_staggered']))
+
+ # check for test pass
+ if (diff_ux > COMPARISON_THRESH):
+ test_pass = False
+ assert test_pass, "diff_ux"
+
+ if (diff_uy > COMPARISON_THRESH):
+ test_pass = False
+ assert test_pass, "diff_uy"
+
diff --git a/tests/test_pstd_elastic_2d_compare_binary_and_cartesian_sensor_mask.py b/tests/test_pstd_elastic_2d_compare_binary_and_cartesian_sensor_mask.py
new file mode 100644
index 000000000..498515619
--- /dev/null
+++ b/tests/test_pstd_elastic_2d_compare_binary_and_cartesian_sensor_mask.py
@@ -0,0 +1,266 @@
+"""
+Unit test to compare cartesian and binary sensor masks.
+"""
+
+import numpy as np
+from copy import deepcopy
+import pytest
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic2D import pstd_elastic_2d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.conversion import cart2grid
+from kwave.utils.mapgen import make_circle
+from kwave.utils.signals import reorder_binary_sensor_data
+
+@pytest.mark.skip(reason="2D not ready")
+def test_pstd_elastic_2d_compare_binary_and_cartesian_sensor_mask():
+
+ # set comparison threshold
+ comparison_thresh = 1e-14
+
+ # set pass variable
+ test_pass = True
+
+ # create the computational grid
+ Nx: int = 128 # [grid points]
+ Ny: int = 128 # [grid points]
+ dx = 25e-3 / float(Nx) # [m]
+ dy = dx # [m]
+ kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))
+
+ # define the properties of the propagation medium
+ sound_speed_compression = 1500.0 * np.ones((Nx, Ny)) # [m/s]
+ sound_speed_shear = np.zeros((Nx, Ny))
+ density = 1000.0 * np.ones((Nx, Ny))
+ medium = kWaveMedium(sound_speed_compression,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear,
+ density=density)
+ medium.sound_speed_shear[Nx // 2 - 1:, :] = 1200.0
+ medium.sound_speed_compression[Nx // 2 - 1:, :] = 2000.0
+ medium.density[Nx // 2 - 1:, :] = 1200.0
+
+ # define source mask
+ source = kSource()
+ p0 = np.zeros((Nx, Ny))
+ p0[21, Ny // 4 - 1:3 * Ny // 4] = 1
+ source._p0 = p0
+
+ # record all output variables
+ sensor = kSensor()
+ # sensor.record = ['p',
+ # 'p_max',
+ # 'p_min',
+ # 'p_rms',
+ # 'u',
+ # 'u_max',
+ # 'u_min',
+ # 'u_rms',
+ # 'u_non_staggered',
+ # 'I',
+ # 'I_avg']
+
+ sensor.record = ['p',
+ 'p_max',
+ 'p_min',
+ 'p_rms']
+
+ # define Cartesian sensor points using points exactly on the grid
+ circ_mask = make_circle(Vector([Nx, Ny]), Vector([Nx // 2, Ny // 2]), int(Nx // 2 - 10))
+ x_points = kgrid.x[circ_mask == 1]
+ y_points = kgrid.y[circ_mask == 1]
+ sensor.mask = np.vstack((x_points, y_points))
+ print(np.shape(x_points),
+ np.shape(y_points),
+ np.shape(np.hstack((x_points, y_points))),
+ np.shape(np.vstack((x_points, y_points))) )
+
+ # # run the simulation as normal
+ # simulation_options_c_ln = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ # cart_interp='linear',
+ # kelvin_voigt_model=False)
+
+ # sensor_data_c_ln = pstd_elastic_2d(kgrid=deepcopy(kgrid),
+ # medium=deepcopy(medium),
+ # sensor=deepcopy(sensor),
+ # source=deepcopy(source),
+ # simulation_options=deepcopy(simulation_options_c_ln))
+
+ # # run the simulation using nearest-neighbour interpolation
+ # simulation_options_c_nn = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ # cart_interp='nearest')
+ # sensor_data_c_nn = pstd_elastic_2d(kgrid=deepcopy(kgrid),
+ # medium=deepcopy(medium),
+ # source=deepcopy(source),
+ # sensor=deepcopy(sensor),
+ # simulation_options=deepcopy(simulation_options_c_nn))
+
+ # convert sensor mask
+ sensor.mask, order_index, reorder_index = cart2grid(kgrid, sensor.mask)
+
+ print(np.shape(order_index), np.shape(reorder_index), np.shape(sensor.mask), sensor.mask.ndim)
+
+ # run the simulation again
+ simulation_options_b = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ cart_interp='linear',
+ kelvin_voigt_model=False)
+ sensor_data_b = pstd_elastic_2d(kgrid=deepcopy(kgrid),
+ medium=deepcopy(medium),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ simulation_options=deepcopy(simulation_options_b))
+
+ # reorder the binary sensor data
+ print(Nx, Ny, kgrid.Nt)
+ # sensor_data_b['p'] = reorder_binary_sensor_data(np.reshape(sensor_data_b['p'], (Nx,Ny)), reorder_index)
+ sensor_data_b['p_max'] = reorder_binary_sensor_data(np.reshape(sensor_data_b['p_max'], np.shape(sensor.mask)), reorder_index)
+ # sensor_data_b['p_min'] = reorder_binary_sensor_data(np.reshape(sensor_data_b['p_min'], (Nx,Ny)), reorder_index)
+ # sensor_data_b['p_rms'] = reorder_binary_sensor_data(np.reshape(sensor_data_b['p_rms'], (Nx,Ny)), reorder_index)
+
+ # sensor_data_b.ux = reorder_binary_sensor_data(sensor_data_b.ux, reorder_index)
+ # sensor_data_b.uy = reorder_binary_sensor_data(sensor_data_b.uy, reorder_index)
+ # sensor_data_b.ux_max = reorder_binary_sensor_data(sensor_data_b.ux_max, reorder_index)
+ # sensor_data_b.uy_max = reorder_binary_sensor_data(sensor_data_b.uy_max, reorder_index)
+ # sensor_data_b.ux_min = reorder_binary_sensor_data(sensor_data_b.ux_min, reorder_index)
+ # sensor_data_b.uy_min = reorder_binary_sensor_data(sensor_data_b.uy_min, reorder_index)
+ # sensor_data_b.ux_rms = reorder_binary_sensor_data(sensor_data_b.ux_rms, reorder_index)
+ # sensor_data_b.uy_rms = reorder_binary_sensor_data(sensor_data_b.uy_rms, reorder_index)
+ # sensor_data_b.ux_non_staggered = reorder_binary_sensor_data(sensor_data_b.ux_non_staggered, reorder_index)
+ # sensor_data_b.uy_non_staggered = reorder_binary_sensor_data(sensor_data_b.uy_non_staggered, reorder_index)
+
+ # sensor_data_b['Ix'] = reorder_binary_sensor_data(sensor_data_b['Ix'], reorder_index)
+ # sensor_data_b['Iy'] = reorder_binary_sensor_data(sensor_data_b['Iy'], reorder_index)
+ # sensor_data_b['Ix_avg'] = reorder_binary_sensor_data(sensor_data_b['Ix_avg'], reorder_index)
+ # sensor_data_b['Iy_avg'] = reorder_binary_sensor_data(sensor_data_b['Iy_avg'], reorder_index)
+
+ # compute errors
+ err_p_nn = np.max(np.abs(sensor_data_c_nn['p'] - sensor_data_b['p'])) / np.max(np.abs(sensor_data_b['p']))
+ if (err_p_nn > comparison_thresh):
+ test_pass = False
+ assert test_pass, "failure with sensor_data_c_nn.p - sensor_data_b.p"
+
+ err_p_ln = np.max(np.abs(sensor_data_c_ln['p'] - sensor_data_b['p'])) / np.max(np.abs(sensor_data_b['p']))
+ if (err_p_ln > comparison_thresh):
+ test_pass = False
+ assert test_pass, "failure with sensor_data_c_ln.p - sensor_data_b.p"
+
+ err_p_max_nn = np.max(np.abs(sensor_data_c_nn['p_max'] - sensor_data_b['p_max'])) / np.max(np.abs(sensor_data_b['p_max']))
+ if (err_p_max_nn > comparison_thresh):
+ test_pass = False
+ assert test_pass, "failure with sensor_data_c_ln.p_max - sensor_data_b.pmax"
+
+ err_p_max_ln = np.max(np.abs(sensor_data_c_ln['p_max'] - sensor_data_b['p_max'])) / np.max(np.abs(sensor_data_b['p_max']))
+ if (err_p_max_ln > comparison_thresh):
+ test_pass = False
+ assert test_pass, "failure with sensor_data_c_ln.p - sensor_data_b.p"
+
+ err_p_min_nn = np.max(np.abs(sensor_data_c_nn['p_min'] - sensor_data_b['p_min'])) / np.max(np.abs(sensor_data_b['p_min']))
+ if (err_p_min_nn > comparison_thresh):
+ test_pass = False
+ assert test_pass, "failure with sensor_data_c_ln.p - sensor_data_b.p"
+
+ err_p_min_ln = np.max(np.abs(sensor_data_c_ln['p_min']- sensor_data_b['p_min'])) / np.max(np.abs(sensor_data_b['p_min']))
+ if (err_p_min_ln > comparison_thresh):
+ test_pass = False
+ assert test_pass, "failure with sensor_data_c_ln.p - sensor_data_b.p"
+
+ err_p_rms_nn = np.max(np.abs(sensor_data_c_nn['p_rms']- sensor_data_b['p_rms'])) / np.max(np.abs(sensor_data_b['p_rms']))
+ if (err_p_rms_nn > comparison_thresh):
+ test_pass = False
+ assert test_pass, "failure with sensor_data_c_ln.p - sensor_data_b.p"
+
+ err_p_rms_ln = np.max(np.abs(sensor_data_c_ln['p_rms']- sensor_data_b['p_rms'])) / np.max(np.abs(sensor_data_b['p_rms']))
+ if (err_p_rms_ln > comparison_thresh):
+ test_pass = False
+ assert test_pass, "failure with sensor_data_c_ln.p - sensor_data_b.p"
+
+ # err_ux_nn = np.max(np.abs(sensor_data_c_nn.ux- sensor_data_b.ux)) / np.max(np.abs(sensor_data_b.ux))
+ # err_ux_ln = np.max(np.abs(sensor_data_c_ln.ux- sensor_data_b.ux)) / np.max(np.abs(sensor_data_b.ux))
+
+ # err_uy_nn = np.max(np.abs(sensor_data_c_nn.uy- sensor_data_b.uy)) / np.max(np.abs(sensor_data_b.uy))
+ # err_uy_ln = np.max(np.abs(sensor_data_c_ln.uy- sensor_data_b.uy)) / np.max(np.abs(sensor_data_b.uy))
+
+ # err_ux_max_nn = np.max(np.abs(sensor_data_c_nn.ux_max- sensor_data_b.ux_max)) / np.max(np.abs(sensor_data_b.ux_max))
+ # err_ux_max_ln = np.max(np.abs(sensor_data_c_ln.ux_max- sensor_data_b.ux_max)) / np.max(np.abs(sensor_data_b.ux_max))
+
+ # err_uy_max_nn = np.max(np.abs(sensor_data_c_nn.uy_max- sensor_data_b.uy_max)) / np.max(np.abs(sensor_data_b.uy_max))
+ # err_uy_max_ln = np.max(np.abs(sensor_data_c_ln.uy_max- sensor_data_b.uy_max)) / np.max(np.abs(sensor_data_b.uy_max))
+
+ # err_ux_min_nn = np.max(np.abs(sensor_data_c_nn.ux_min- sensor_data_b.ux_min)) / np.max(np.abs(sensor_data_b.ux_min))
+ # err_ux_min_ln = np.max(np.abs(sensor_data_c_ln.ux_min- sensor_data_b.ux_min)) / np.max(np.abs(sensor_data_b.ux_min))
+
+ # err_uy_min_nn = np.max(np.abs(sensor_data_c_nn.uy_min- sensor_data_b.uy_min)) / np.max(np.abs(sensor_data_b.uy_min))
+ # err_uy_min_ln = np.max(np.abs(sensor_data_c_ln.uy_min- sensor_data_b.uy_min)) / np.max(np.abs(sensor_data_b.uy_min))
+
+ # err_ux_rms_nn = np.max(np.abs(sensor_data_c_nn.ux_rms- sensor_data_b.ux_rms)) / np.max(np.abs(sensor_data_b.ux_rms))
+ # err_ux_rms_ln = np.max(np.abs(sensor_data_c_ln.ux_rms- sensor_data_b.ux_rms)) / np.max(np.abs(sensor_data_b.ux_rms))
+
+ # err_uy_rms_nn = np.max(np.abs(sensor_data_c_nn.uy_rms- sensor_data_b.uy_rms)) / np.max(np.abs(sensor_data_b.uy_rms))
+ # err_uy_rms_ln = np.max(np.abs(sensor_data_c_ln.uy_rms- sensor_data_b.uy_rms)) / np.max(np.abs(sensor_data_b.uy_rms))
+
+ # err_ux_non_staggered_nn = np.max(np.abs(sensor_data_c_nn.ux_non_staggered- sensor_data_b.ux_non_staggered)) / np.max(np.abs(sensor_data_b.ux_non_staggered))
+ # err_ux_non_staggered_ln = np.max(np.abs(sensor_data_c_ln.ux_non_staggered- sensor_data_b.ux_non_staggered)) / np.max(np.abs(sensor_data_b.ux_non_staggered))
+
+ # err_uy_non_staggered_nn = np.max(np.abs(sensor_data_c_nn.uy_non_staggered- sensor_data_b.uy_non_staggered)) / np.max(np.abs(sensor_data_b.uy_non_staggered))
+ # err_uy_non_staggered_ln = np.max(np.abs(sensor_data_c_ln.uy_non_staggered- sensor_data_b.uy_non_staggered)) / np.max(np.abs(sensor_data_b.uy_non_staggered))
+
+ # err_Ix_nn = np.max(np.abs(sensor_data_c_nn['Ix']- sensor_data_b['Ix'])) / np.max(np.abs(sensor_data_b['Ix']))
+ # err_Ix_ln = np.max(np.abs(sensor_data_c_ln['Ix']- sensor_data_b['Ix'])) / np.max(np.abs(sensor_data_b['Ix']))
+
+ # err_Iy_nn = np.max(np.abs(sensor_data_c_nn['Iy']- sensor_data_b['Iy'])) / np.max(np.abs(sensor_data_b['Iy']))
+ # err_Iy_ln = np.max(np.abs(sensor_data_c_ln['Iy']- sensor_data_b['Iy'])) / np.max(np.abs(sensor_data_b['Iy']))
+
+ # err_Ix_avg_nn = np.max(np.abs(sensor_data_c_nn['Ix_avg']- sensor_data_b['Ix_avg'])) / np.max(np.abs(sensor_data_b['Ix_avg']))
+ # err_Ix_avg_ln = np.max(np.abs(sensor_data_c_ln['Ix_avg']- sensor_data_b['Ix_avg'])) / np.max(np.abs(sensor_data_b['Ix_avg']))
+
+ # err_Iy_avg_nn = np.max(np.abs(sensor_data_c_nn['Iy_avg']- sensor_data_b['Iy_avg'])) / np.max(np.abs(sensor_data_b['Iy_avg']))
+ # err_Iy_avg_ln = np.max(np.abs(sensor_data_c_ln['Iy_avg']- sensor_data_b['Iy_avg'])) / np.max(np.abs(sensor_data_b['Iy_avg']))
+
+ # # check for test pass
+ # if (err_p_nn > comparison_thresh) || ...
+ # (err_p_ln > comparison_thresh) || ...
+ # (err_p_max_nn > comparison_thresh) || ...
+ # (err_p_max_ln > comparison_thresh) || ...
+ # (err_p_min_nn > comparison_thresh) || ...
+ # (err_p_min_ln > comparison_thresh) || ...
+ # (err_p_rms_nn > comparison_thresh) || ...
+ # (err_p_rms_ln > comparison_thresh) || ...
+ # (err_ux_nn > comparison_thresh) || ...
+ # (err_ux_ln > comparison_thresh) || ...
+ # (err_ux_ln > comparison_thresh) || ...
+ # (err_ux_max_nn > comparison_thresh) || ...
+ # (err_ux_max_ln > comparison_thresh) || ...
+ # (err_ux_min_nn > comparison_thresh) || ...
+ # (err_ux_min_ln > comparison_thresh) || ...
+ # (err_ux_rms_nn > comparison_thresh) || ...
+ # (err_ux_rms_ln > comparison_thresh) || ...
+ # (err_ux_non_staggered_nn > comparison_thresh) || ...
+ # (err_ux_non_staggered_ln > comparison_thresh) || ...
+ # (err_uy_nn > comparison_thresh) || ...
+ # (err_uy_ln > comparison_thresh) || ...
+ # (err_uy_max_nn > comparison_thresh) || ...
+ # (err_uy_max_ln > comparison_thresh) || ...
+ # (err_uy_min_nn > comparison_thresh) || ...
+ # (err_uy_min_ln > comparison_thresh) || ...
+ # (err_uy_rms_nn > comparison_thresh) || ...
+ # (err_uy_rms_ln > comparison_thresh) || ...
+ # (err_uy_non_staggered_nn > comparison_thresh) || ...
+ # (err_uy_non_staggered_ln > comparison_thresh) || ...
+ # (err_Ix_nn > comparison_thresh) || ...
+ # (err_Ix_ln > comparison_thresh) || ...
+ # (err_Ix_avg_nn > comparison_thresh) || ...
+ # (err_Ix_avg_ln > comparison_thresh) || ...
+ # (err_Iy_nn > comparison_thresh) || ...
+ # (err_Iy_ln > comparison_thresh) || ...
+ # (err_Iy_avg_nn > comparison_thresh) || ...
+ # (err_Iy_avg_ln > comparison_thresh)
+ # test_pass = False
+
+
+
+
diff --git a/tests/test_pstd_elastic_2d_compare_binary_and_cuboid_sensor_mask.py b/tests/test_pstd_elastic_2d_compare_binary_and_cuboid_sensor_mask.py
new file mode 100644
index 000000000..f4e255b4d
--- /dev/null
+++ b/tests/test_pstd_elastic_2d_compare_binary_and_cuboid_sensor_mask.py
@@ -0,0 +1,185 @@
+"""
+Unit test to compare the simulation results using a labelled and binary source mask
+"""
+
+import numpy as np
+from copy import deepcopy
+import pytest
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic2D import pstd_elastic_2d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.mapgen import make_disc
+
+@pytest.mark.skip(reason="2D not ready")
+def test_pstd_elastic_2d_compare_binary_and_cuboid_sensor_mask():
+
+ # set pass variable
+ test_pass: bool = True
+
+ # set additional literals to give further permutations of the test
+ comparison_threshold: float = 1e-15
+ pml_inside: bool = True
+
+ # create the computational grid
+ Nx: int = 128 # number of grid points in the x direction
+ Ny: int = 128 # number of grid points in the y direction
+ dx: float = 0.1e-3 # grid point spacing in the x direction [m]
+ dy: float = 0.1e-3 # grid point spacing in the y direction [m]
+ kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))
+
+ # define the properties of the upper layer of the propagation medium
+ sound_speed_compression = 1500.0 * np.ones((Nx, Ny)) # [m/s]
+ sound_speed_shear = np.zeros((Nx, Ny)) # [m/s]
+ density = 1000.0 * np.ones((Nx, Ny)) # [kg/m^3]
+
+ medium = kWaveMedium(sound_speed_compression,
+ density=density,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear)
+
+ # define the properties of the lower layer of the propagation medium
+ medium.sound_speed_compression[Nx // 2 - 1:, :] = 2000.0 # [m/s]
+ medium.sound_speed_shear[Nx // 2 - 1:, :] = 800.0 # [m/s]
+ medium.density[Nx // 2 - 1:, :] = 1200.0 # [kg/m^3]
+
+ # create initial pressure distribution using makeDisc
+ disc_magnitude = 5.0 # [Pa]
+ disc_x_pos: int = 30 # [grid points]
+ disc_y_pos: int = 64 # [grid points]
+ disc_radius: int = 5 # [grid points]
+
+ source = kSource()
+ source.p0 = disc_magnitude * make_disc(Vector([Nx, Ny]), Vector([disc_x_pos, disc_y_pos]), disc_radius)
+
+ # define list of cuboid corners using two intersecting cuboids
+ cuboid_corners = [[40, 10, 90, 65], [10, 60, 50, 70]]
+
+ sensor = kSensor()
+ sensor.mask = cuboid_corners
+
+ # set the variables to record
+ sensor.record = ['p', 'p_max', 'p_min', 'p_rms', 'p_max_all', 'p_min_all', 'p_final',
+ 'u', 'u_max', 'u_min', 'u_rms', 'u_max_all', 'u_min_all', 'u_final',
+ 'u_non_staggered', 'I', 'I_avg']
+
+ # run the simulation as normal
+ simulation_options_cuboids = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=pml_inside,
+ kelvin_voigt_model=False)
+
+ sensor_data_cuboids = pstd_elastic_2d(deepcopy(kgrid),
+ deepcopy(medium),
+ deepcopy(source),
+ deepcopy(sensor),
+ deepcopy(simulation_options_cuboids))
+
+ # create a binary mask for display from the list of corners
+ sensor.mask = np.zeros(np.shape(kgrid.k))
+
+ cuboid_index: int = 0
+ sensor.mask[cuboid_corners[0, cuboid_index]:cuboid_corners[2, cuboid_index] + 1,
+ cuboid_corners[1, cuboid_index]:cuboid_corners[3, cuboid_index] + 1] = 1
+
+ # run the simulation
+ simulation_options_comp1 = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=pml_inside,
+ kelvin_voigt_model=False)
+
+ sensor_data_comp1 = pstd_elastic_2d(deepcopy(kgrid),
+ deepcopy(medium),
+ deepcopy(source),
+ deepcopy(sensor),
+ deepcopy(simulation_options_comp1))
+
+ # compute the error from the first cuboid
+ L_inf_p = np.max(np.abs(sensor_data_cuboids[cuboid_index].p - sensor_data_comp1.p)) / np.max(np.abs(sensor_data_comp1.p))
+ L_inf_p_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_max - sensor_data_comp1.p_max)) / np.max(np.abs(sensor_data_comp1.p_max))
+ L_inf_p_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_min - sensor_data_comp1.p_min)) / np.max(np.abs(sensor_data_comp1.p_min))
+ L_inf_p_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_rms - sensor_data_comp1.p_rms)) / np.max(np.abs(sensor_data_comp1.p_rms))
+
+ L_inf_ux = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux - sensor_data_comp1.ux)) / np.max(np.abs(sensor_data_comp1.ux))
+ L_inf_ux_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_max - sensor_data_comp1.ux_max)) / np.max(np.abs(sensor_data_comp1.ux_max))
+ L_inf_ux_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_min - sensor_data_comp1.ux_min)) / np.max(np.abs(sensor_data_comp1.ux_min))
+ L_inf_ux_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_rms - sensor_data_comp1.ux_rms)) / np.max(np.abs(sensor_data_comp1.ux_rms))
+
+ L_inf_uy = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy - sensor_data_comp1.uy)) / np.max(np.abs(sensor_data_comp1.uy))
+ L_inf_uy_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_max - sensor_data_comp1.uy_max)) / np.max(np.abs(sensor_data_comp1.uy_max))
+ L_inf_uy_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_min - sensor_data_comp1.uy_min)) / np.max(np.abs(sensor_data_comp1.uy_min))
+ L_inf_uy_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_rms - sensor_data_comp1.uy_rms)) / np.max(np.abs(sensor_data_comp1.uy_rms))
+
+ # compute the error from the total variables
+ L_inf_p_max_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_max_all - sensor_data_comp1.p_max_all)) / np.max(np.abs(sensor_data_comp1.p_max_all))
+ L_inf_ux_max_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_max_all - sensor_data_comp1.ux_max_all)) / np.max(np.abs(sensor_data_comp1.ux_max_all))
+ L_inf_uy_max_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_max_all - sensor_data_comp1.uy_max_all)) / np.max(np.abs(sensor_data_comp1.uy_max_all))
+
+ L_inf_p_min_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_min_all - sensor_data_comp1.p_min_all)) / np.max(np.abs(sensor_data_comp1.p_min_all))
+ L_inf_ux_min_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_min_all - sensor_data_comp1.ux_min_all)) / np.max(np.abs(sensor_data_comp1.ux_min_all))
+ L_inf_uy_min_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_min_all - sensor_data_comp1.uy_min_all)) / np.max(np.abs(sensor_data_comp1.uy_min_all))
+
+ L_inf_p_final = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_final - sensor_data_comp1.p_final)) / np.max(np.abs(sensor_data_comp1.p_final))
+ L_inf_ux_final = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_final - sensor_data_comp1.ux_final)) / np.max(np.abs(sensor_data_comp1.ux_final))
+ L_inf_uy_final = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_final - sensor_data_comp1.uy_final)) / np.max(np.abs(sensor_data_comp1.uy_final))
+
+ # get maximum error
+ L_inf_max = np.max([L_inf_p, L_inf_p_max, L_inf_p_min, L_inf_p_rms, L_inf_ux,
+ L_inf_ux_max, L_inf_ux_min, L_inf_ux_rms, L_inf_uy, L_inf_uy_max,
+ L_inf_uy_min, L_inf_uy_rms, L_inf_p_max_all, L_inf_ux_max_all,
+ L_inf_uy_max_all, L_inf_p_min_all, L_inf_ux_min_all, L_inf_uy_min_all,
+ L_inf_p_final, L_inf_ux_final, L_inf_uy_final])
+
+ # compute pass
+ if (L_inf_max > comparison_threshold):
+ test_pass = False
+ assert test_pass, "fails here"
+
+ # ------------------------
+
+ # create a binary mask for display from the list of corners
+ sensor.mask = np.zeros(np.shape(kgrid.k))
+
+ cuboid_index = 1
+ sensor.mask[cuboid_corners[0, cuboid_index]:cuboid_corners[2, cuboid_index],
+ cuboid_corners[1, cuboid_index]:cuboid_corners[3, cuboid_index]] = 1
+
+ # run the simulation
+ simulation_options_comp2 = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=pml_inside,
+ kelvin_voigt_model=False)
+
+ sensor_data_comp2 = pstd_elastic_2d(deepcopy(kgrid),
+ deepcopy(medium),
+ deepcopy(source),
+ deepcopy(sensor),
+ deepcopy(simulation_options_comp2))
+
+ # compute the error from the second cuboid
+ L_inf_p = np.max(np.abs(sensor_data_cuboids[cuboid_index].p - sensor_data_comp2.p)) / np.max(np.abs(sensor_data_comp2.p))
+ L_inf_p_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_max - sensor_data_comp2.p_max)) / np.max(np.abs(sensor_data_comp2.p_max))
+ L_inf_p_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_min - sensor_data_comp2.p_min)) / np.max(np.abs(sensor_data_comp2.p_min))
+ L_inf_p_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_rms - sensor_data_comp2.p_rms)) / np.max(np.abs(sensor_data_comp2.p_rms))
+
+ L_inf_ux = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux - sensor_data_comp2.ux)) / np.max(np.abs(sensor_data_comp2.ux))
+ L_inf_ux_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_max - sensor_data_comp2.ux_max)) / np.max(np.abs(sensor_data_comp2.ux_max))
+ L_inf_ux_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_min - sensor_data_comp2.ux_min)) / np.max(np.abs(sensor_data_comp2.ux_min))
+ L_inf_ux_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_rms - sensor_data_comp2.ux_rms)) / np.max(np.abs(sensor_data_comp2.ux_rms))
+
+ L_inf_uy = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy - sensor_data_comp2.uy)) / np.max(np.abs(sensor_data_comp2.uy))
+ L_inf_uy_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_max - sensor_data_comp2.uy_max)) / np.max(np.abs(sensor_data_comp2.uy_max))
+ L_inf_uy_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_min - sensor_data_comp2.uy_min)) / np.max(np.abs(sensor_data_comp2.uy_min))
+ L_inf_uy_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_rms - sensor_data_comp2.uy_rms)) / np.max(np.abs(sensor_data_comp2.uy_rms))
+
+ # get maximum error
+ L_inf_max = np.max([L_inf_p, L_inf_p_max, L_inf_p_min, L_inf_p_rms,
+ L_inf_ux, L_inf_ux_max, L_inf_ux_min, L_inf_ux_rms,
+ L_inf_uy, L_inf_uy_max, L_inf_uy_min, L_inf_uy_rms])
+
+ # compute pass
+ if (L_inf_max > comparison_threshold):
+ test_pass = False
+
+ assert test_pass, "fails at this point"
diff --git a/tests/test_pstd_elastic_2d_compare_labelled_and_binary_source_mask.py b/tests/test_pstd_elastic_2d_compare_labelled_and_binary_source_mask.py
new file mode 100644
index 000000000..cd4e325e9
--- /dev/null
+++ b/tests/test_pstd_elastic_2d_compare_labelled_and_binary_source_mask.py
@@ -0,0 +1,182 @@
+"""
+Unit test to compare the simulation results using a labelled and binary source mask
+"""
+
+import numpy as np
+from copy import deepcopy
+import pytest
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic2D import pstd_elastic_2d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.mapgen import make_multi_arc
+
+@pytest.mark.skip(reason="2D not ready")
+def pstd_elastic_2d_compare_labelled_and_binary_source_mask():
+
+ # set pass variable
+ test_pass: bool = True
+
+ # set additional literals to give further permutations of the test
+ comparison_threshold: float = 1e-15
+ pml_inside: bool = False
+
+ # create the computational grid
+ Nx: int = 216 # number of grid points in the x direction
+ Ny: int = 216 # number of grid points in the y direction
+ dx = 50e-3 / float(Nx) # grid point spacing in the x direction [m]
+ dy = dx # grid point spacing in the y direction [m]
+ kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))
+
+ # define the properties of the upper layer of the propagation medium
+ sound_speed_compression = 1500.0 * np.ones((Nx, Ny)) # [m/s]
+ sound_speed_shear = np.zeros((Nx, Ny)) # [m/s]
+ density = 1000.0 * np.ones((Nx, Ny)) # [kg/m^3]
+
+ medium = kWaveMedium(sound_speed_compression,
+ density=density,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear)
+
+ t_end = 20e-6
+ kgrid.makeTime(medium.sound_speed_compression, t_end=t_end)
+
+ # define a curved transducer element
+ arc_pos = np.array([[30, 30], [150, 30], [150, 200]], dtype=int)
+ radius = np.array([20, 30, 40], dtype=int)
+ diameter = np.array([21, 15, 31], dtype=int)
+ focus_pos = np.arary([Nx // 2, Ny // 2], dtype=int)
+ binary_mask, labelled_mask = make_multi_arc(Vector([Nx, Ny]), arc_pos, radius, diameter, focus_pos)
+
+ # define a time varying sinusoidal source
+ source_freq = 0.25e6 # [Hz]
+ source_mag = 0.5 # [Pa]
+ source_1 = source_mag * np.sin(2.0 * np.pi * source_freq * kgrid.t_array)
+
+ source_freq = 1e6 # [Hz]
+ source_mag = 0.8 # [Pa]
+ source_2 = source_mag * np.sin(2.0 * np.pi * source_freq * kgrid.t_array)
+
+ source_freq = 0.05e6 # [Hz]
+ source_mag = 0.2 # [Pa]
+ source_3 = source_mag * np.sin(2.0 * np.pi * source_freq * kgrid.t_array)
+
+ # assemble sources
+ labelled_sources = np.empty((3, kgrid.Nt))
+ labelled_sources[0, :] = np.squeeze(source_1)
+ labelled_sources[1, :] = np.squeeze(source_2)
+ labelled_sources[2, :] = np.squeeze(source_3)
+
+ # assign sources for labelled source mask
+ source = kSource()
+ source.s_mask = labelled_mask
+ source.sxx = labelled_sources
+ source.syy = labelled_sources
+ source.sxy = labelled_sources
+
+ # create a sensor mask covering the entire computational domain using the
+ # opposing corners of a rectangle
+ sensor = kSensor()
+ sensor.mask = [1, 1, Nx, Ny]
+
+ # set the record mode capture the final wave-field and the statistics at
+ # each sensor point
+ sensor.record = ['p_final', 'p_max']
+
+ simulation_options_labelled = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=pml_inside)
+
+ sensor_data_labelled = pstd_elastic_2d(deepcopy(kgrid),
+ deepcopy(medium),
+ deepcopy(source),
+ deepcopy(sensor),
+ deepcopy(simulation_options_labelled))
+
+ # reassign the source using a binary source mask
+ source.s_mask = binary_mask
+ index_mask = labelled_mask[labelled_mask != 0]
+ source.sxx = labelled_sources[index_mask, :]
+ source.syy = source.sxx
+ source.sxy = source.sxx
+
+ # run the simulation using the a binary source mask
+ simulation_options_binary = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=pml_inside)
+
+ sensor_data_binary = pstd_elastic_2d(deepcopy(kgrid),
+ deepcopy(medium),
+ deepcopy(source),
+ deepcopy(sensor),
+ deepcopy(simulation_options_binary))
+
+ # # compute the error from the first cuboid
+ # L_inf_final = np.max(np.abs(sensor_data_labelled.p_final - sensor_data_binary.p_final)) / np. max(np.abs(sensor_data_binary.p_final))
+ # L_inf_max = np.max(np.abs(sensor_data_labelled.p_max - sensor_data_binary.p_max)) / np. max(np.abs(sensor_data_binary.p_max))
+
+ # # compute pass
+ # if (L_inf_max > comparison_threshold) or (L_inf_final > comparison_threshold):
+ # test_pass = False
+
+ L_inf_max = np.max(np.abs(sensor_data_labelled['p_max'] - sensor_data_binary['p_max'])) / np.max(np.abs(sensor_data_binary['p_max']))
+ if (L_inf_max > comparison_threshold):
+ test_pass = False
+ assert test_pass, "L_inf_max, stress source"
+
+ L_inf_final = np.max(np.abs(sensor_data_labelled['p_final'] - sensor_data_binary['p_final'])) / np.max(np.abs(sensor_data_binary['p_final']))
+ if (L_inf_final > comparison_threshold):
+ test_pass = False
+ assert test_pass, "L_inf_final, stress source"
+
+
+ # ----------------------------------------
+
+ # repeat for velocity source
+ del source
+ source = kSource()
+ source.u_mask = labelled_mask
+ source.ux = labelled_sources * 1e-6
+ source.uy = source.ux
+
+ # run the simulation using the labelled source mask
+ simulation_options_labelled = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=pml_inside)
+
+ sensor_data_labelled = pstd_elastic_2d(deepcopy(kgrid),
+ deepcopy(medium),
+ deepcopy(source),
+ deepcopy(sensor),
+ deepcopy(simulation_options_labelled))
+
+ # reassign the source using a binary source mask
+ del source
+ source = kSource()
+ source.u_mask = binary_mask
+ index_mask = labelled_mask[labelled_mask != 0]
+ source.ux = labelled_sources[index_mask, :] * 1e-6
+ source.uy = source.ux
+
+ # run the simulation using the a binary source mask
+ simulation_options_binary = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=pml_inside)
+
+ sensor_data_binary = pstd_elastic_2d(deepcopy(kgrid),
+ deepcopy(medium),
+ deepcopy(source),
+ deepcopy(sensor),
+ deepcopy(simulation_options_binary))
+
+ # compute the error from the first cuboid
+ L_inf_max = np.max(np.abs(sensor_data_labelled['p_max'] - sensor_data_binary['p_max'])) / np.max(np.abs(sensor_data_binary['p_max']))
+ if (L_inf_max > comparison_threshold):
+ test_pass = False
+ assert test_pass, "L_inf_max, velocity source"
+
+ L_inf_final = np.max(np.abs(sensor_data_labelled['p_final'] - sensor_data_binary['p_final'])) / np.max(np.abs(sensor_data_binary['p_final']))
+ if (L_inf_final > comparison_threshold):
+ test_pass = False
+ assert test_pass, "L_inf_final, velocity source"
+
diff --git a/tests/test_pstd_elastic_2d_compare_with_kspaceFirstOrder2D.py b/tests/test_pstd_elastic_2d_compare_with_kspaceFirstOrder2D.py
new file mode 100644
index 000000000..bbfafa1a0
--- /dev/null
+++ b/tests/test_pstd_elastic_2d_compare_with_kspaceFirstOrder2D.py
@@ -0,0 +1,338 @@
+"""
+Unit test to compare that the elastic code with the shear wave speed set to
+zero gives the same answers as the regular fluid code in k-Wave.
+"""
+
+import numpy as np
+from copy import deepcopy
+# import pytest
+import matplotlib.pyplot as plt
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic2D import pstd_elastic_2d
+from kwave.kspaceFirstOrder2D import kspace_first_order_2d_gpu
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.options.simulation_execution_options import SimulationExecutionOptions
+from kwave.utils.filters import filter_time_series
+from kwave.utils.mapgen import make_disc
+
+#from scipy.io import loadmat
+
+
+#@pytest.mark.skip(reason="2D not ready")
+def test_pstd_elastic_2d_compare_with_kspaceFirstOrder2D():
+
+ # set additional literals to give further permutations of the test
+ HETEROGENEOUS: bool = True
+ USE_PML: bool = False
+ COMPARISON_THRESH = 5e-10
+
+ # option to skip the first point in the time series (for p0 sources, there
+ # is a strange bug where there is a high error for the first stored time
+ # point)
+ COMP_START_INDEX: int = 1
+
+ # set pass variable
+ test_pass: bool = True
+
+ # =========================================================================
+ # SIMULATION
+ # =========================================================================
+
+ # create the computational grid
+ Nx: int = 96 # number of grid points in the x (row) direction
+ Ny: int = 192 # number of grid points in the y (column) direction
+ dx: float = 0.1e-3 # grid point spacing in the x direction [m]
+ dy: float = 0.1e-3 # grid point spacing in the y direction [m]
+ kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))
+
+ # define the medium properties
+ cp: float = 1500.0
+ cs: float = 0.0
+ rho: float = 1000.0
+
+ # create the time array
+ CFL: float = 0.1
+ t_end: float = 7e-6
+ kgrid.makeTime(cp, CFL, t_end)
+
+ # create and assign the medium properties
+ if HETEROGENEOUS:
+ # elastic medium
+ sound_speed_compression = cp * np.ones((Nx, Ny))
+ sound_speed_compression[Nx // 2 - 1:, :] = 2.0 * cp
+ sound_speed_shear = cs * np.ones((Nx, Ny))
+ density = rho * np.ones((Nx, Ny))
+ medium_elastic = kWaveMedium(sound_speed=sound_speed_compression,
+ density=density,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear)
+ # fluid medium
+ sound_speed = cp * np.ones((Nx, Ny))
+ sound_speed[Nx // 2 - 1:, :] = 2.0 * cp
+ medium_fluid = kWaveMedium(sound_speed,
+ density=density)
+ else:
+ # elastic medium
+ medium_elastic = kWaveMedium(sound_speed=cp,
+ density=rho,
+ sound_speed_compression=cp,
+ sound_speed_shear=cs)
+ # fluid medium
+ medium_fluid = kWaveMedium(sound_speed=cp,
+ density=rho)
+
+ # test names
+ test_names = ['source.p0',
+ 'source.p, additive',
+ 'source.p, dirichlet',
+ 'source.ux, additive',
+ 'source.ux, dirichlet',
+ 'source.uy, additive',
+ 'source.uy, dirichlet'
+ ]
+
+ # define a single point sensor
+ sensor_elastic = kSensor()
+ sensor_fluid = kSensor()
+ sensor_elastic.mask = np.zeros((Nx, Ny), dtype=bool)
+ sensor_elastic.mask[3 * Nx // 4 - 1, 3 * Ny // 4 - 1] = True
+ sensor_fluid.mask = np.zeros((Nx, Ny), dtype=bool)
+ sensor_fluid.mask[3 * Nx // 4 - 1, 3 * Ny // 4 - 1] = True
+
+ # set some things to record
+ sensor_elastic.record = ['p', 'p_final', 'u', 'u_final']
+ sensor_fluid.record = ['p', 'p_final', 'u', 'u_final']
+
+ # loop through tests
+ for test_num, test_name in enumerate(test_names):
+
+ source_fluid = kSource()
+ source_elastic = kSource()
+
+ x_pos: int = 30 # [grid points]
+ y_pos: int = Ny // 2 # [grid points]
+
+ # update command line
+ print('Running Number: ', test_num, ':', test_name)
+
+ if test_name == 'source.p0':
+ # create initial pressure distribution using makeDisc
+ disc_magnitude: float = 5.0 # [Pa]
+ disc_radius: int = 6 # [grid points]
+ p0 = disc_magnitude * make_disc(Vector([Nx, Ny]), Vector([x_pos, y_pos]), disc_radius).astype(float)
+ source_fluid.p0 = p0
+ # create equivalent elastic source
+ source_elastic.p0 = p0
+
+ elif test_name == 'source.p, additive' or test_name == 'source.p, dirichlet':
+ # create pressure source
+ source_fluid.p_mask = np.zeros((Nx, Ny), dtype=bool)
+ freq: float = 2.0 * np.pi * 1e6
+ magnitude: float = 5.0 # [Pa]
+ source_fluid.p_mask[x_pos, y_pos] = bool
+ p = magnitude * np.sin(freq * np.squeeze(kgrid.t_array))
+ source_fluid.p = filter_time_series(deepcopy(kgrid), deepcopy(medium_fluid), p)
+ # create equivalent elastic source
+ source_elastic.s_mask = source_fluid.p_mask
+ source_elastic.sxx = -deepcopy(source_fluid.p)
+ source_elastic.syy = -deepcopy(source_fluid.p)
+
+ elif test_name == 'source.ux, additive' or test_name == 'source.ux, dirichlet':
+ # create velocity source
+ source_fluid.u_mask = np.zeros((Nx, Ny), dtype=bool)
+ source_fluid.u_mask[x_pos, y_pos] = True
+ ux = 5.0 * np.sin(2.0 * np.pi * 1e6 * np.squeeze(kgrid.t_array)) / (cp * rho)
+ source_fluid.ux = filter_time_series(deepcopy(kgrid), deepcopy(medium_fluid), ux)
+ # create equivalent elastic source
+ source_elastic = deepcopy(source_fluid)
+
+ elif test_name == 'source.uy, additive' or test_name == 'source.uy, dirichlet':
+ # create velocity source
+ source_fluid.u_mask = np.zeros((Nx, Ny), dtype=bool)
+ source_fluid.u_mask[x_pos, y_pos] = True
+ uy = 5.0 * np.sin(2.0 * np.pi * 1e6 * np.squeeze(kgrid.t_array)) / (cp * rho)
+ source_fluid.uy = filter_time_series(deepcopy(kgrid), deepcopy(medium_fluid), uy)
+ # create equivalent elastic source
+ source_elastic = deepcopy(source_fluid)
+
+ # set source mode
+ if test_name == 'source.p, additive':
+ source_fluid.p_mode = 'additive'
+ source_elastic.s_mode = 'additive'
+ elif test_name == 'source.p, dirichlet':
+ source_fluid.p_mode = 'dirichlet'
+ source_elastic.s_mode = 'dirichlet'
+ elif test_name == 'source.ux, additive' or test_name == 'source.uy, additive':
+ source_fluid.u_mode = 'additive'
+ source_elastic.u_mode = 'additive'
+ elif test_name == 'source.ux, dirichlet' or test_name == 'source.uy, dirichlet':
+ source_fluid.u_mode = 'dirichlet'
+ source_elastic.u_mode = 'dirichlet'
+
+ # options for writing to file, but not doing simulations
+ input_filename_p = 'data_p_input.h5'
+ output_filename_p = 'data_p_output.h5'
+ DATA_CAST: str = 'single'
+ DATA_PATH = '.'
+
+ if not USE_PML:
+ simulation_options_elastic = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_alpha=0.0)
+ simulation_options_fluid = SimulationOptions(simulation_type=SimulationType.FLUID,
+ data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename_p,
+ output_filename=output_filename_p,
+ data_path=DATA_PATH,
+ use_kspace=False,
+ pml_alpha=0.0,
+ hdf_compression_level='lzf')
+ else:
+ simulation_options_elastic = SimulationOptions(simulation_type=SimulationType.ELASTIC)
+ simulation_options_fluid = SimulationOptions(simulation_type=SimulationType.FLUID,
+ data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename_p,
+ output_filename=output_filename_p,
+ data_path=DATA_PATH,
+ use_kspace=False,
+ hdf_compression_level='lzf')
+
+ # options for executing simulations
+ execution_options_fluid = SimulationExecutionOptions(is_gpu_simulation=True, delete_data=False)
+
+ # run the fluid simulation
+ sensor_data_fluid = kspace_first_order_2d_gpu(medium=deepcopy(medium_fluid),
+ kgrid=deepcopy(kgrid),
+ source=deepcopy(source_fluid),
+ sensor=deepcopy(sensor_fluid),
+ simulation_options=deepcopy(simulation_options_fluid),
+ execution_options=deepcopy(execution_options_fluid))
+
+ # run the simulations
+ sensor_data_elastic = pstd_elastic_2d(medium=deepcopy(medium_elastic),
+ kgrid=deepcopy(kgrid),
+ source=deepcopy(source_elastic),
+ sensor=deepcopy(sensor_elastic),
+ simulation_options=deepcopy(simulation_options_elastic))
+
+ # reshape data to fit
+ sensor_data_elastic['p_final'] = np.transpose(sensor_data_elastic['p_final'])
+ sensor_data_elastic['p_final'] = sensor_data_elastic['p_final'].reshape(sensor_data_elastic['p_final'].shape, order='F')
+
+ sensor_data_elastic['ux_final'] = np.transpose(sensor_data_elastic['ux_final'])
+ sensor_data_elastic['ux_final'] = sensor_data_elastic['ux_final'].reshape(sensor_data_elastic['ux_final'].shape, order='F')
+
+ sensor_data_elastic['uy_final'] = np.transpose(sensor_data_elastic['uy_final'])
+ sensor_data_elastic['uy_final'] = sensor_data_elastic['uy_final'].reshape(sensor_data_elastic['uy_final'].shape, order='F')
+
+ # compute comparisons for time series
+ L_inf_p = np.max(np.abs(np.squeeze(sensor_data_elastic['p'])[COMP_START_INDEX:] - sensor_data_fluid['p'][COMP_START_INDEX:])) / np.max(np.abs(sensor_data_fluid['p'][COMP_START_INDEX:]))
+ L_inf_ux = np.max(np.abs(np.squeeze(sensor_data_elastic['ux'])[COMP_START_INDEX:] - sensor_data_fluid['ux'][COMP_START_INDEX:])) / np.max(np.abs(sensor_data_fluid['ux'][COMP_START_INDEX:]))
+ L_inf_uy = np.max(np.abs(np.squeeze(sensor_data_elastic['uy'])[COMP_START_INDEX:] - sensor_data_fluid['uy'][COMP_START_INDEX:])) / np.max(np.abs(sensor_data_fluid['uy'][COMP_START_INDEX:]))
+
+ # compuate comparisons for field
+ L_inf_p_final = np.max(np.abs(sensor_data_elastic['p_final'] - sensor_data_fluid['p_final'])) / np.max(np.abs(sensor_data_fluid['p_final']))
+ L_inf_ux_final = np.max(np.abs(sensor_data_elastic['ux_final'] - sensor_data_fluid['ux_final'])) / np.max(np.abs(sensor_data_fluid['ux_final']))
+ L_inf_uy_final = np.max(np.abs(sensor_data_elastic['uy_final'] - sensor_data_fluid['uy_final'])) / np.max(np.abs(sensor_data_fluid['uy_final']))
+
+ # compute pass
+ latest_test: bool = False
+ if ((L_inf_p < COMPARISON_THRESH) and (L_inf_ux < COMPARISON_THRESH) and
+ (L_inf_uy < COMPARISON_THRESH) and (L_inf_p_final < COMPARISON_THRESH) and
+ (L_inf_ux_final < COMPARISON_THRESH) and (L_inf_uy_final < COMPARISON_THRESH)):
+ # set test variable
+ latest_test = True
+ else:
+ print('fails')
+
+ if (L_inf_p < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_p =', L_inf_p)
+
+ if (L_inf_ux < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_ux =', L_inf_ux)
+
+ if (L_inf_uy < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_uy =', L_inf_uy)
+
+ if (L_inf_p_final < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_p_final =', L_inf_p_final)
+
+ if (L_inf_ux_final < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_ux_final =', L_inf_ux_final)
+
+ if (L_inf_uy_final < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_uy_final =', L_inf_uy_final)
+
+ test_pass = test_pass and latest_test
+
+ ###########
+
+
+ print('\t', np.squeeze(sensor_data_elastic['p'])[-4:], '\n\t', sensor_data_fluid['p'][-4:])
+ print(str(np.squeeze(sensor_data_elastic['ux'])[-4:]) + '\n' + str(sensor_data_fluid['ux'][-4:]))
+ print('\t', np.squeeze(sensor_data_elastic['uy'])[-4:], '\n\t', sensor_data_fluid['uy'][-4:])
+
+ fig1, ((ax1a, ax1b, ax1c,)) = plt.subplots(3, 1)
+ fig1.suptitle(f"{test_name}: Comparisons")
+ # if test_num == 0:
+ # ax1a.plot(np.squeeze(sensor_data_elastic['p'])[COMP_START_INDEX:], 'r-o', sensor_data_fluid['p'][COMP_START_INDEX:], 'b--*',
+ # np.squeeze(matlab_sensor_fluid_p), 'k--o', np.squeeze(matlab_sensor_elastic_p), 'k-+',)
+ # else:
+ ax1a.plot(np.squeeze(sensor_data_elastic['p'])[COMP_START_INDEX:], 'r-o', sensor_data_fluid['p'][COMP_START_INDEX:], 'b--*')
+ ax1b.plot(np.squeeze(sensor_data_elastic['ux'])[COMP_START_INDEX:], 'r-o', sensor_data_fluid['ux'][COMP_START_INDEX:], 'b--*')
+ ax1c.plot(np.squeeze(sensor_data_elastic['uy'])[COMP_START_INDEX:], 'r-o', sensor_data_fluid['uy'][COMP_START_INDEX:], 'b--*')
+
+ # fig2, ((ax2a, ax2b, ax2c)) = plt.subplots(3, 1)
+ # fig2.suptitle(f"{test_name}: Errors")
+ # ax2a.plot(np.abs(np.squeeze(sensor_data_elastic['p'])[COMP_START_INDEX:] - sensor_data_fluid['p'][COMP_START_INDEX:]))
+ # ax2b.plot(np.abs(np.squeeze(sensor_data_elastic['ux'])[COMP_START_INDEX:] - sensor_data_fluid['ux'][COMP_START_INDEX:]))
+ # ax2c.plot(np.abs(np.squeeze(sensor_data_elastic['uy'])[COMP_START_INDEX:] - sensor_data_fluid['uy'][COMP_START_INDEX:]))
+
+ # if test_num == 0:
+ # fig4, ((ax4a, ax4b, ax4c)) = plt.subplots(3, 1)
+ # ax4a.imshow(source_fluid.p0.astype(float))
+ # ax4b.imshow(matlab_source_fluid_p0.astype(float))
+ # ax4c.imshow(source_fluid.p0.astype(float) - matlab_source_fluid_p0.astype(float))
+
+ # fig5, (ax5a, ax5b) = plt.subplots(1, 2)
+ # ax5a.imshow(mat_sxx[:,0].reshape(p0.shape, order='F') )
+ # ax5b.imshow(sxx[:,0].reshape(p0.shape, order='F') )
+
+ # fig3, ((ax3a, ax3b, ax3c), (ax3d, ax3e, ax3f)) = plt.subplots(2, 3)
+ # fig3.suptitle(f"{test_name}: Final Values")
+ # ax3a.imshow(sensor_data_elastic['p_final'])
+ # ax3b.imshow(sensor_data_elastic['ux_final'])
+ # ax3c.imshow(sensor_data_elastic['uy_final'])
+ # ax3d.imshow(sensor_data_fluid['p_final'])
+ # ax3e.imshow(sensor_data_fluid['ux_final'])
+ # ax3f.imshow(sensor_data_fluid['uy_final'])
+
+ # clear structures
+ del source_fluid
+ del source_elastic
+ del sensor_data_elastic
+ del sensor_data_fluid
+
+ plt.show()
+
+ assert test_pass, "not working"
diff --git a/tests/test_pstd_elastic_3d_check_mpml_stability.py b/tests/test_pstd_elastic_3d_check_mpml_stability.py
new file mode 100644
index 000000000..f0ff8253c
--- /dev/null
+++ b/tests/test_pstd_elastic_3d_check_mpml_stability.py
@@ -0,0 +1,132 @@
+"""
+Unit test to test the stability of the pml and m-pml
+"""
+
+import numpy as np
+from copy import deepcopy
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic3D import pstd_elastic_3d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.signals import tone_burst
+from kwave.utils.mapgen import make_spherical_section
+
+def test_pstd_elastic_3d_check_mpml_stability():
+
+ test_pass: bool = True
+
+ # create the computational grid
+ PML_SIZE: int = 10
+ Nx: int = 80 - 2 * PML_SIZE
+ Ny: int = 64 - 2 * PML_SIZE
+ Nz: int = 64 - 2 * PML_SIZE
+ dx: float = 0.1e-3
+ dy: float = 0.1e-3
+ dz: float = 0.1e-3
+ kgrid = kWaveGrid(Vector([Nx, Ny, Nz]), Vector([dx, dy, dz]))
+
+ # define the properties of the upper layer of the propagation medium
+ sound_speed_compression = 1500.0 * np.ones((Nx, Ny, Nz)) # [m/s]
+ sound_speed_shear = np.zeros((Nx, Ny, Nz)) # [m/s]
+ density = 1000.0 * np.ones((Nx, Ny, Nz)) # [kg/m^3]
+ # define the properties of the lower layer of the propagation medium
+ sound_speed_compression[Nx // 2 - 1:, :, :] = 2000.0 # [m/s]
+ sound_speed_shear[Nx // 2 - 1:, :, :] = 1000.0 # [m/s]
+ density[Nx // 2 - 1:, :, :] = 1200.0 # [kg/m^3]
+ medium = kWaveMedium(sound_speed_compression,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear,
+ density=density)
+
+ # create the time array
+ cfl = 0.3 # Courant-Friedrichs-Lewy number
+ t_end = 8e-6 # [s]
+ kgrid.makeTime(medium.sound_speed_compression.max(), cfl, t_end)
+
+ # define the source mask
+ s_rad: int = 15
+ s_height: int = 8
+ offset: int = 15
+ ss, _ = make_spherical_section(s_rad, s_height)
+
+ source = kSource()
+ ss_width: int = np.shape(ss)[1]
+ ss_half_width: int = np.floor(ss_width / 2).astype(int)
+ y_start_pos: int = Ny // 2 - ss_half_width - 1
+ y_end_pos: int = y_start_pos + ss_width
+ z_start_pos: int = Nz // 2 - ss_half_width - 1
+ z_end_pos: int = z_start_pos + ss_width
+
+ source.s_mask = np.zeros((Nx, Ny, Nz), dtype=int)
+
+ source.s_mask[offset:s_height + offset, y_start_pos:y_end_pos, z_start_pos:z_end_pos] = ss.astype(int)
+ source.s_mask[:, :, Nz // 2 - 1:] = int(0)
+
+ # define the source signal
+ fs = 1.0 / kgrid.dt
+ source.sxx = tone_burst(sample_freq=fs, signal_freq=1e6, num_cycles=3)
+
+ source.syy = deepcopy(source.sxx)
+ source.szz = deepcopy(source.sxx)
+
+ # define sensor
+ sensor = kSensor()
+ sensor.mask = np.ones((Nx, Ny, Nz), dtype=bool)
+ sensor.record = ['u_final']
+
+ # define input arguments
+ simulation_options_pml = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ kelvin_voigt_model=False,
+ use_sensor=True,
+ pml_inside=False,
+ pml_size=PML_SIZE,
+ blank_sensor=True,
+ binary_sensor_mask=True,
+ multi_axial_PML_ratio=0.0)
+
+ # run the simulations
+ sensor_data_pml = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ medium=deepcopy(medium),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ simulation_options=deepcopy(simulation_options_pml))
+
+ simulation_options_mpml = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ kelvin_voigt_model=False,
+ use_sensor=True,
+ pml_inside=False,
+ pml_size=PML_SIZE,
+ blank_sensor=True,
+ binary_sensor_mask=True,
+ multi_axial_PML_ratio=0.1)
+
+ sensor_data_mpml = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ medium=deepcopy(medium),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ simulation_options=deepcopy(simulation_options_mpml))
+
+ # check magnitudes
+ pml_max = np.max([sensor_data_pml.ux_final, sensor_data_pml.uy_final, sensor_data_pml.uz_final])
+ mpml_max = np.max([sensor_data_mpml.ux_final, sensor_data_mpml.uy_final, sensor_data_mpml.uz_final])
+
+ # set reference magnitude (initial source)
+ ref_max = 1.0 / (np.max(medium.sound_speed_shear) * np.max(medium.density))
+
+ # check results - the test should fail if the pml DOES work (i.e., it
+ # doesn't become unstable), or if the m-pml DOESN'T work (i.e., it does
+ # become unstable). The pml should not work and the mpml should.
+ if pml_max < ref_max:
+ test_pass = False
+ assert test_pass, "pml_max < ref_max " + str(pml_max < ref_max) + ", pml_max: " + str(pml_max) + ", ref_max: " + str(ref_max)
+
+ if mpml_max > ref_max:
+ test_pass = False
+ assert test_pass, "mpml_max > ref_max " + str(mpml_max > ref_max) + ", mpml_max: " + str(mpml_max) + ", ref_max: " + str(ref_max)
+
+
+
diff --git a/tests/test_pstd_elastic_3d_check_split_field.py b/tests/test_pstd_elastic_3d_check_split_field.py
new file mode 100644
index 000000000..24708f121
--- /dev/null
+++ b/tests/test_pstd_elastic_3d_check_split_field.py
@@ -0,0 +1,158 @@
+
+"""
+Unit test to check that the split field components sum to give the correct field, e.g., ux = ux^p + ux^s.
+"""
+
+import numpy as np
+from copy import deepcopy
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic3D import pstd_elastic_3d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.signals import tone_burst
+from kwave.utils.mapgen import make_bowl
+
+def test_pstd_elastic_3d_check_split_field():
+
+ # set comparison threshold
+ COMPARISON_THRESH: float = 1e-15
+
+ # set pass variable
+ test_pass: bool = True
+
+ # =========================================================================
+ # SIMULATION PARAMETERS
+ # =========================================================================
+
+ # create the computational grid
+ PML_size: int = 10 # [grid points]
+ Nx: int = 64 - 2 * PML_size # [grid points]
+ Ny: int = 64 - 2 * PML_size # [grid points]
+ Nz: int = 64 - 2 * PML_size # [grid points]
+ dx: float = 0.5e-3 # [m]
+ dy: float = 0.5e-3 # [m]
+ dz: float = 0.5e-3 # [m]
+ kgrid = kWaveGrid(Vector([Nx, Ny, Nz]), Vector([dx, dy, dz]))
+
+ # define the medium properties for the top layer
+ cp1: float = 1540.0 # compressional wave speed [m/s]
+ cs1: float = 0.0 # shear wave speed [m/s]
+ rho1: float = 1000.0 # density [kg/m^3]
+ alpha0_p1: float = 0.1 # compressional absorption [dB/(MHz^2 cm)]
+ alpha0_s1: float = 0.1 # shear absorption [dB/(MHz^2 cm)]
+
+ # define the medium properties for the bottom layer
+ cp2: float = 3000.0 # compressional wave speed [m/s]
+ cs2: float = 1400.0 # shear wave speed [m/s]
+ rho2: float = 1850.0 # density [kg/m^3]
+ alpha0_p2: float = 1.0 # compressional absorption [dB/(MHz^2 cm)]
+ alpha0_s2: float = 1.0 # shear absorption [dB/(MHz^2 cm)]
+
+ # create the time array
+ cfl: float = 0.1
+ t_end: float = 15e-6
+ kgrid.makeTime(cp1, cfl, t_end)
+
+ # define position of heterogeneous slab
+ slab = np.zeros((Nx, Ny, Nz))
+ slab[Nx // 2 - 1:, :, :] = 1
+
+ # define the source geometry in SI units (where 0, 0 is the grid center)
+ bowl_pos = [-6e-3, -6e-3, -6e-3] # [m]
+ focus_pos = [5e-3, 5e-3, 5e-3] # [m]
+ radius = 15e-3 # [m]
+ diameter = 10e-3 # [m]
+
+ # define the driving signal
+ source_freq = 500e3 # [Hz]
+ source_strength = 1e6 # [Pa]
+ source_cycles = 3 # number of tone burst cycles
+
+ # define the sensor to record the maximum particle velocity everywhere
+ sensor = kSensor()
+ sensor.record = ['p', 'u_split_field', 'u_non_staggered']
+ sensor.mask = np.zeros((Nx, Ny, Nz))
+ sensor.mask[:, :, Nz // 2 - 1] = 1
+
+ # convert the source parameters to grid points
+ bowl_pos = np.round(np.asarray(bowl_pos) / dx).astype(int) + np.asarray([Nx // 2 - 1, Ny // 2 - 1, Nz // 2 - 1])
+ focus_pos = np.round(np.asarray(focus_pos) / dx).astype(int) + np.asarray([Nx // 2 - 1, Ny // 2 - 1, Nz // 2 - 1])
+ radius = int(round(radius / dx))
+ diameter = int(round(diameter / dx))
+
+ # force the diameter to be odd
+ if diameter % 2 == 0:
+ diameter: int = diameter + int(1)
+
+ # define the medium properties
+ sound_speed_compression = cp1 * np.ones((Nx, Ny, Nz))
+ sound_speed_shear = cs1 * np.ones((Nx, Ny, Nz))
+ density = rho1 * np.ones((Nx, Ny, Nz))
+ alpha_coeff_compression = alpha0_p1 * np.ones((Nx, Ny, Nz))
+ alpha_coeff_shear = alpha0_s1 * np.ones((Nx, Ny, Nz))
+
+ medium = kWaveMedium(sound_speed_compression,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear,
+ density=density,
+ alpha_coeff_compression=alpha_coeff_compression,
+ alpha_coeff_shear=alpha_coeff_shear)
+
+ medium.sound_speed_compression[slab == 1] = cp2
+ medium.sound_speed_shear[slab == 1] = cs2
+ medium.density[slab == 1] = rho2
+ medium.alpha_coeff_compression[slab == 1] = alpha0_p2
+ medium.alpha_coeff_shear[slab == 1] = alpha0_s2
+
+ # generate the source geometry
+ source_mask = make_bowl(Vector([Nx, Ny, Nz]), Vector(bowl_pos), radius, diameter, Vector(focus_pos))
+
+ # assign the source
+ source = kSource()
+ source.s_mask = source_mask
+ fs = 1.0 / kgrid.dt
+ source.sxx = -source_strength * tone_burst(fs, source_freq, source_cycles)
+ source.syy = source.sxx
+ source.szz = source.sxx
+
+ simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=False,
+ pml_size=PML_size,
+ kelvin_voigt_model=False)
+
+ # run the elastic simulation
+ sensor_data_elastic = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ medium=deepcopy(medium),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ simulation_options=deepcopy(simulation_options))
+
+ # compute errors
+ diff_ux = np.max(np.abs(sensor_data_elastic['ux_non_staggered'] -
+ sensor_data_elastic['ux_split_p'] -
+ sensor_data_elastic['ux_split_s'])) / np.max(np.abs(sensor_data_elastic['ux_non_staggered']))
+
+ diff_uy = np.max(np.abs(sensor_data_elastic['uy_non_staggered'] -
+ sensor_data_elastic['uy_split_p'] -
+ sensor_data_elastic['uy_split_s'])) / np.max(np.abs(sensor_data_elastic['uy_non_staggered']))
+
+ diff_uz = np.max(np.abs(sensor_data_elastic['uz_non_staggered'] -
+ sensor_data_elastic['uz_split_p'] -
+ sensor_data_elastic['uz_split_s'])) / np.max(np.abs(sensor_data_elastic['uz_non_staggered']))
+
+ # check for test pass
+ if (diff_ux > COMPARISON_THRESH):
+ test_pass = False
+ assert test_pass, "diff_ux: " + str(diff_ux)
+
+ if (diff_uy > COMPARISON_THRESH):
+ test_pass = False
+ assert test_pass, "diff_uy: " + str(diff_uy)
+
+ if (diff_uz > COMPARISON_THRESH):
+ test_pass = False
+ assert test_pass, "diff_uz: " + str(diff_uz)
\ No newline at end of file
diff --git a/tests/test_pstd_elastic_3d_compare_binary_and_cartesian_sensor_mask.py b/tests/test_pstd_elastic_3d_compare_binary_and_cartesian_sensor_mask.py
new file mode 100644
index 000000000..5d4c3ddfc
--- /dev/null
+++ b/tests/test_pstd_elastic_3d_compare_binary_and_cartesian_sensor_mask.py
@@ -0,0 +1,345 @@
+"""
+Unit test to compare cartesian and binary sensor masks
+"""
+
+import numpy as np
+from copy import deepcopy
+import pytest
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic3D import pstd_elastic_3d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.conversion import cart2grid
+from kwave.utils.mapgen import make_sphere, make_multi_bowl
+from kwave.utils.signals import reorder_binary_sensor_data
+from kwave.utils.filters import filter_time_series
+
+@pytest.mark.skip(reason="not ready")
+def test_pstd_elastic_3d_compare_binary_and_cartesian_sensor_mask():
+
+ # set comparison threshold
+ comparison_thresh: float = 1e-14
+
+ # set pass variable
+ test_pass: bool = True
+
+ # create the computational grid
+ Nx: int = 48
+ Ny: int = 48
+ Nz: int = 48
+ dx: float = 25e-3 / float(Nx)
+ dy: float = 25e-3 / float(Ny)
+ dz: float = 25e-3 / float(Nz)
+ kgrid = kWaveGrid(Vector([Nx, Ny, Nz]), Vector([dx, dy, dz]))
+
+
+
+ # define the properties of the propagation medium
+ sound_speed_compression = 1500.0 * np.ones((Nx, Ny, Nz)) # [m/s]
+ sound_speed_compression[Nx // 2 - 1:, :, :] = 2000.0
+
+ sound_speed_shear = np.zeros((Nx, Ny, Nz)) # [m/s]
+ sound_speed_shear[Nx // 2 - 1:, :, :] = 1400
+
+ density = 1000.0 * np.ones((Nx, Ny, Nz))
+ density[Nx // 2 - 1:, :, :] = 1200.0
+
+ medium = kWaveMedium(sound_speed_compression,
+ density=density,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear)
+
+ # create the time array using default CFL condition
+ cfl: float = 0.1
+ kgrid.makeTime(medium.sound_speed_compression, cfl=cfl)
+
+ # define source mask
+ # source = kSource()
+ # p0 = np.zeros((Nx, Ny, Nz), dtype=bool)
+ # p0[7,
+ # Ny // 4 - 1:3 * Ny // 4,
+ # Nz // 4 - 1:3 * Nz // 4] = True
+ # source.p0 = p0
+
+ source_freq_0 = 1e6 # [Hz]
+ source_mag_0 = 0.5 # [Pa]
+ source_0 = source_mag_0 * np.sin(2.0 * np.pi * source_freq_0 * np.squeeze(kgrid.t_array))
+ source_0 = filter_time_series(kgrid, medium, deepcopy(source_0))
+
+ source_freq_1 = 3e6 # [Hz]
+ source_mag_1 = 0.8 # [Pa]
+ source_1 = source_mag_1 * np.sin(2.0 * np.pi * source_freq_1 * np.squeeze(kgrid.t_array))
+ source_1 = filter_time_series(kgrid, medium, deepcopy(source_1))
+
+ # assemble sources
+ labelled_sources = np.zeros((2, kgrid.Nt))
+ labelled_sources[0, :] = np.squeeze(source_0)
+ labelled_sources[1, :] = np.squeeze(source_1)
+
+ # define multiple curved transducer elements
+ bowl_pos = np.array([(19.0, 19.0, Nz / 2.0 - 1.0), (48.0, 48.0, Nz / 2.0 - 1.0)])
+ bowl_radius = np.array([20.0, 15.0])
+ bowl_diameter = np.array([int(15), int(21)], dtype=np.uint8)
+ bowl_focus = np.array([(int(31), int(31), int(31))], dtype=np.uint8)
+
+ binary_mask, labelled_mask = make_multi_bowl(Vector([Nx, Ny, Nz]), bowl_pos, bowl_radius, bowl_diameter, bowl_focus)
+
+ # create ksource object
+ source = kSource()
+
+ # source mask is from the labelled mask
+ source.s_mask = deepcopy(labelled_mask)
+
+ # assign sources from labelled source
+ source.sxx = deepcopy(labelled_sources)
+ source.syy = deepcopy(labelled_sources)
+ source.szz = deepcopy(labelled_sources)
+ source.sxy = deepcopy(labelled_sources)
+ source.sxz = deepcopy(labelled_sources)
+ source.syz = deepcopy(labelled_sources)
+
+
+ sensor = kSensor()
+
+ # define Cartesian sensor points using points exactly on the grid
+ sphere_mask = make_sphere(Vector([Nx, Ny, Nz]), radius=10)
+ x_points = kgrid.x[sphere_mask == 1]
+ y_points = kgrid.y[sphere_mask == 1]
+ z_points = kgrid.z[sphere_mask == 1]
+ sensor.mask = np.vstack((x_points, y_points, z_points))
+
+ # record all output variables
+ sensor.record = ['p', 'p_max', 'p_min', 'p_rms', 'u', 'u_max', 'u_min', 'u_rms',
+ 'u_non_staggered', 'I', 'I_avg']
+
+ # run the simulation as normal
+ simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_size=6,
+ kelvin_voigt_model=False)
+
+ sensor_data_c_ln = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+ # run the simulation using nearest-neighbour interpolation
+ simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_size=6,
+ cart_interp='nearest',
+ kelvin_voigt_model=False)
+
+ sensor_data_c_nn = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+ # convert sensor mask
+ _, _, reorder_index = cart2grid(kgrid, sensor.mask)
+
+ simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_size=int(6),
+ kelvin_voigt_model=False)
+
+ # run the simulation again
+ sensor_data_b = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+ # reorder the binary sensor data
+ sensor_data_b.p = reorder_binary_sensor_data(sensor_data_b.p, reorder_index)
+ # sensor_data_b.p_max = reorder_binary_sensor_data(sensor_data_b.p_max, reorder_index)
+ # sensor_data_b.p_min = reorder_binary_sensor_data(sensor_data_b.p_min, reorder_index)
+ # sensor_data_b.p_rms = reorder_binary_sensor_data(sensor_data_b.p_rms, reorder_index)
+ # sensor_data_b.ux = reorder_binary_sensor_data(sensor_data_b.ux, reorder_index)
+ # sensor_data_b.uy = reorder_binary_sensor_data(sensor_data_b.uy, reorder_index)
+ # sensor_data_b.uz = reorder_binary_sensor_data(sensor_data_b.uz, reorder_index)
+ # sensor_data_b.ux_max = reorder_binary_sensor_data(sensor_data_b.ux_max, reorder_index)
+ # sensor_data_b.uy_max = reorder_binary_sensor_data(sensor_data_b.uy_max, reorder_index)
+ # sensor_data_b.uz_max = reorder_binary_sensor_data(sensor_data_b.uz_max, reorder_index)
+ # sensor_data_b.ux_min = reorder_binary_sensor_data(sensor_data_b.ux_min, reorder_index)
+ # sensor_data_b.uy_min = reorder_binary_sensor_data(sensor_data_b.uy_min, reorder_index)
+ # sensor_data_b.uz_min = reorder_binary_sensor_data(sensor_data_b.uz_min, reorder_index)
+ # sensor_data_b.ux_rms = reorder_binary_sensor_data(sensor_data_b.ux_rms, reorder_index)
+ # sensor_data_b.uy_rms = reorder_binary_sensor_data(sensor_data_b.uy_rms, reorder_index)
+ # sensor_data_b.uz_rms = reorder_binary_sensor_data(sensor_data_b.uz_rms, reorder_index)
+ # sensor_data_b.ux_non_staggered = reorder_binary_sensor_data(sensor_data_b.ux_non_staggered, reorder_index)
+ # sensor_data_b.uy_non_staggered = reorder_binary_sensor_data(sensor_data_b.uy_non_staggered, reorder_index)
+ # sensor_data_b.uz_non_staggered = reorder_binary_sensor_data(sensor_data_b.uz_non_staggered, reorder_index)
+ # sensor_data_b.Ix = reorder_binary_sensor_data(sensor_data_b.Ix, reorder_index)
+ # sensor_data_b.Iy = reorder_binary_sensor_data(sensor_data_b.Iy, reorder_index)
+ # sensor_data_b.Iz = reorder_binary_sensor_data(sensor_data_b.Iz, reorder_index)
+ # sensor_data_b.Ix_avg = reorder_binary_sensor_data(sensor_data_b.Ix_avg, reorder_index)
+ # sensor_data_b.Iy_avg = reorder_binary_sensor_data(sensor_data_b.Iy_avg, reorder_index)
+ # sensor_data_b.Iz_avg = reorder_binary_sensor_data(sensor_data_b.Iz_avg, reorder_index)
+
+ # compute errors
+ err_p_nn = np.max(np.abs(sensor_data_c_nn.p - sensor_data_b.p)) / np.max(np.abs(sensor_data_b.p))
+ err_p_ln = np.max(np.abs(sensor_data_c_ln.p - sensor_data_b.p)) / np.max(np.abs(sensor_data_b.p))
+
+ # err_p_max_nn = np.max(np.abs(sensor_data_c_nn.p_max - sensor_data_b.p_max)) / np.max(np.abs(sensor_data_b.p_max))
+ # err_p_max_ln = np.max(np.abs(sensor_data_c_ln.p_max - sensor_data_b.p_max)) / np.max(np.abs(sensor_data_b.p_max))
+
+ # err_p_min_nn = np.max(np.abs(sensor_data_c_nn.p_min - sensor_data_b.p_min)) / np.max(np.abs(sensor_data_b.p_min))
+ # err_p_min_ln = np.max(np.abs(sensor_data_c_ln.p_min - sensor_data_b.p_min)) / np.max(np.abs(sensor_data_b.p_min))
+
+ # err_p_rms_nn = np.max(np.abs(sensor_data_c_nn.p_rms - sensor_data_b.p_rms)) / np.max(np.abs(sensor_data_b.p_rms))
+ # err_p_rms_ln = np.max(np.abs(sensor_data_c_ln.p_rms - sensor_data_b.p_rms)) / np.max(np.abs(sensor_data_b.p_rms))
+
+ # err_ux_nn = np.max(np.abs(sensor_data_c_nn.ux - sensor_data_b.ux)) / np.max(np.abs(sensor_data_b.ux))
+ # err_ux_ln = np.max(np.abs(sensor_data_c_ln.ux - sensor_data_b.ux)) / np.max(np.abs(sensor_data_b.ux))
+
+ # err_uy_nn = np.max(np.abs(sensor_data_c_nn.uy - sensor_data_b.uy)) / np.max(np.abs(sensor_data_b.uy))
+ # err_uy_ln = np.max(np.abs(sensor_data_c_ln.uy - sensor_data_b.uy)) / np.max(np.abs(sensor_data_b.uy))
+
+ # err_uz_nn = np.max(np.abs(sensor_data_c_nn.uz - sensor_data_b.uz)) / np.max(np.abs(sensor_data_b.uz))
+ # err_uz_ln = np.max(np.abs(sensor_data_c_ln.uz - sensor_data_b.uz)) / np.max(np.abs(sensor_data_b.uz))
+
+ # err_ux_max_nn = np.max(np.abs(sensor_data_c_nn.ux_max - sensor_data_b.ux_max)) / np.max(np.abs(sensor_data_b.ux_max))
+ # err_ux_max_ln = np.max(np.abs(sensor_data_c_ln.ux_max - sensor_data_b.ux_max)) / np.max(np.abs(sensor_data_b.ux_max))
+
+ # err_uy_max_nn = np.max(np.abs(sensor_data_c_nn.uy_max - sensor_data_b.uy_max)) / np.max(np.abs(sensor_data_b.uy_max))
+ # err_uy_max_ln = np.max(np.abs(sensor_data_c_ln.uy_max - sensor_data_b.uy_max)) / np.max(np.abs(sensor_data_b.uy_max))
+
+ # err_uz_max_nn = np.max(np.abs(sensor_data_c_nn.uz_max - sensor_data_b.uz_max)) / np.max(np.abs(sensor_data_b.uz_max))
+ # err_uz_max_ln = np.max(np.abs(sensor_data_c_ln.uz_max - sensor_data_b.uz_max)) / np.max(np.abs(sensor_data_b.uz_max))
+
+ # err_ux_min_nn = np.max(np.abs(sensor_data_c_nn.ux_min - sensor_data_b.ux_min)) / np.max(np.abs(sensor_data_b.ux_min))
+ # err_ux_min_ln = np.max(np.abs(sensor_data_c_ln.ux_min - sensor_data_b.ux_min)) / np.max(np.abs(sensor_data_b.ux_min))
+
+ # err_uy_min_nn = np.max(np.abs(sensor_data_c_nn.uy_min - sensor_data_b.uy_min)) / np.max(np.abs(sensor_data_b.uy_min))
+ # err_uy_min_ln = np.max(np.abs(sensor_data_c_ln.uy_min - sensor_data_b.uy_min)) / np.max(np.abs(sensor_data_b.uy_min))
+
+ # err_uz_min_nn = np.max(np.abs(sensor_data_c_nn.uz_min - sensor_data_b.uz_min)) / np.max(np.abs(sensor_data_b.uz_min))
+ # err_uz_min_ln = np.max(np.abs(sensor_data_c_ln.uz_min - sensor_data_b.uz_min)) / np.max(np.abs(sensor_data_b.uz_min))
+
+ # err_ux_rms_nn = np.max(np.abs(sensor_data_c_nn.ux_rms - sensor_data_b.ux_rms)) / np.max(np.abs(sensor_data_b.ux_rms))
+ # err_ux_rms_ln = np.max(np.abs(sensor_data_c_ln.ux_rms - sensor_data_b.ux_rms)) / np.max(np.abs(sensor_data_b.ux_rms))
+
+ # err_uy_rms_nn = np.max(np.abs(sensor_data_c_nn.uy_rms - sensor_data_b.uy_rms)) / np.max(np.abs(sensor_data_b.uy_rms))
+ # err_uy_rms_ln = np.max(np.abs(sensor_data_c_ln.uy_rms - sensor_data_b.uy_rms)) / np.max(np.abs(sensor_data_b.uy_rms))
+
+ # err_uz_rms_nn = np.max(np.abs(sensor_data_c_nn.uz_rms - sensor_data_b.uz_rms)) / np.max(np.abs(sensor_data_b.uz_rms))
+ # err_uz_rms_ln = np.max(np.abs(sensor_data_c_ln.uz_rms - sensor_data_b.uz_rms)) / np.max(np.abs(sensor_data_b.uz_rms))
+
+ # err_ux_non_staggered_nn = np.max(np.abs(sensor_data_c_nn.ux_non_staggered - sensor_data_b.ux_non_staggered)) / np.max(np.abs(sensor_data_b.ux_non_staggered))
+ # err_ux_non_staggered_ln = np.max(np.abs(sensor_data_c_ln.ux_non_staggered - sensor_data_b.ux_non_staggered)) / np.max(np.abs(sensor_data_b.ux_non_staggered))
+
+ # err_uy_non_staggered_nn = np.max(np.abs(sensor_data_c_nn.uy_non_staggered - sensor_data_b.uy_non_staggered)) / np.max(np.abs(sensor_data_b.uy_non_staggered))
+ # err_uy_non_staggered_ln = np.max(np.abs(sensor_data_c_ln.uy_non_staggered - sensor_data_b.uy_non_staggered)) / np.max(np.abs(sensor_data_b.uy_non_staggered))
+
+ # err_uz_non_staggered_nn = np.max(np.abs(sensor_data_c_nn.uz_non_staggered - sensor_data_b.uz_non_staggered)) / np.max(np.abs(sensor_data_b.uz_non_staggered))
+ # err_uz_non_staggered_ln = np.max(np.abs(sensor_data_c_ln.uz_non_staggered - sensor_data_b.uz_non_staggered)) / np.max(np.abs(sensor_data_b.uz_non_staggered))
+
+ # err_Ix_nn = np.max(np.abs(sensor_data_c_nn.Ix - sensor_data_b.Ix)) / np.max(np.abs(sensor_data_b.Ix))
+ # err_Ix_ln = np.max(np.abs(sensor_data_c_ln.Ix - sensor_data_b.Ix)) / np.max(np.abs(sensor_data_b.Ix))
+
+ # err_Iy_nn = np.max(np.abs(sensor_data_c_nn.Iy - sensor_data_b.Iy)) / np.max(np.abs(sensor_data_b.Iy))
+ # err_Iy_ln = np.max(np.abs(sensor_data_c_ln.Iy - sensor_data_b.Iy)) / np.max(np.abs(sensor_data_b.Iy))
+
+ # err_Iz_nn = np.max(np.abs(sensor_data_c_nn.Iz - sensor_data_b.Iz)) / np.max(np.abs(sensor_data_b.Iz))
+ # err_Iz_ln = np.max(np.abs(sensor_data_c_ln.Iz - sensor_data_b.Iz)) / np.max(np.abs(sensor_data_b.Iz))
+
+ # err_Ix_avg_nn = np.max(np.abs(sensor_data_c_nn.Ix_avg - sensor_data_b.Ix_avg)) / np.max(np.abs(sensor_data_b.Ix_avg))
+ # err_Ix_avg_ln = np.max(np.abs(sensor_data_c_ln.Ix_avg - sensor_data_b.Ix_avg)) / np.max(np.abs(sensor_data_b.Ix_avg))
+
+ # err_Iy_avg_nn = np.max(np.abs(sensor_data_c_nn.Iy_avg - sensor_data_b.Iy_avg)) / np.max(np.abs(sensor_data_b.Iy_avg))
+ # err_Iy_avg_ln = np.max(np.abs(sensor_data_c_ln.Iy_avg - sensor_data_b.Iy_avg)) / np.max(np.abs(sensor_data_b.Iy_avg))
+
+ # err_Iz_avg_nn = np.max(np.abs(sensor_data_c_nn.Iz_avg - sensor_data_b.Iz_avg)) / np.max(np.abs(sensor_data_b.Iz_avg))
+ # err_Iz_avg_ln = np.max(np.abs(sensor_data_c_ln.Iz_avg - sensor_data_b.Iz_avg)) / np.max(np.abs(sensor_data_b.Iz_avg))
+
+ # check for test pass
+ if ((err_p_nn > comparison_thresh)
+ or (err_p_ln > comparison_thresh)
+ # or (err_p_max_nn > comparison_thresh) or
+ # (err_p_max_ln > comparison_thresh) or
+ # (err_p_min_nn > comparison_thresh) or
+ # (err_p_min_ln > comparison_thresh) or
+ # (err_p_rms_nn > comparison_thresh) or
+ # (err_p_rms_ln > comparison_thresh) or
+ # (err_ux_nn > comparison_thresh) or
+ # (err_ux_ln > comparison_thresh) or
+ # (err_ux_max_nn > comparison_thresh) or
+ # (err_ux_max_ln > comparison_thresh) or
+ # (err_ux_min_nn > comparison_thresh) or
+ # (err_ux_min_ln > comparison_thresh) or
+ # (err_ux_rms_nn > comparison_thresh) or
+ # (err_ux_rms_ln > comparison_thresh) or
+ # (err_ux_non_staggered_nn > comparison_thresh) or
+ # (err_ux_non_staggered_ln > comparison_thresh) or
+ # (err_uy_nn > comparison_thresh) or
+ # (err_uy_ln > comparison_thresh) or
+ # (err_uy_max_nn > comparison_thresh) or
+ # (err_uy_max_ln > comparison_thresh) or
+ # (err_uy_min_nn > comparison_thresh) or
+ # (err_uy_min_ln > comparison_thresh) or
+ # (err_uy_rms_nn > comparison_thresh) or
+ # (err_uy_rms_ln > comparison_thresh) or
+ # (err_uy_non_staggered_nn > comparison_thresh) or
+ # (err_uy_non_staggered_ln > comparison_thresh) or
+ # (err_uz_nn > comparison_thresh) or
+ # (err_uz_ln > comparison_thresh) or
+ # (err_uz_max_nn > comparison_thresh) or
+ # (err_uz_max_ln > comparison_thresh) or
+ # (err_uz_min_nn > comparison_thresh) or
+ # (err_uz_min_ln > comparison_thresh) or
+ # (err_uz_rms_nn > comparison_thresh) or
+ # (err_uz_rms_ln > comparison_thresh) or
+ # (err_uz_non_staggered_nn > comparison_thresh) or
+ # (err_uz_non_staggered_ln > comparison_thresh) or
+ # (err_Ix_nn > comparison_thresh) or
+ # (err_Ix_ln > comparison_thresh) or
+ # (err_Ix_avg_nn > comparison_thresh) or
+ # (err_Ix_avg_ln > comparison_thresh) or
+ # (err_Iy_nn > comparison_thresh) or
+ # (err_Iy_ln > comparison_thresh) or
+ # (err_Iy_avg_nn > comparison_thresh) or
+ # (err_Iy_avg_ln > comparison_thresh) or
+ # (err_Iz_nn > comparison_thresh) or
+ # (err_Iz_ln > comparison_thresh) or
+ # (err_Iz_avg_nn > comparison_thresh) or
+ # (err_Iz_avg_ln > comparison_thresh)
+ ):
+ test_pass = False
+
+ assert test_pass, "Fails"
+
+
+ # # plot
+ # if plot_comparisons
+
+ # figure;
+ # subplot(5, 1, 1);
+ # imagesc(sensor_data_c_ln.p);
+ # colorbar;
+ # title('Cartesian - Linear');
+
+ # subplot(5, 1, 2);
+ # imagesc(sensor_data_c_nn.p);
+ # colorbar;
+ # title('Cartesian - Nearest Neighbour');
+
+ # subplot(5, 1, 3);
+ # imagesc(sensor_data_b.p);
+ # colorbar;
+ # title('Binary');
+
+ # subplot(5, 1, 4);
+ # imagesc(abs(sensor_data_b.p - sensor_data_c_ln.p))
+ # colorbar;
+ # title('Diff (Linear - Binary)');
+
+ # subplot(5, 1, 5);
+ # imagesc(abs(sensor_data_b.p - sensor_data_c_nn.p))
+ # colorbar;
+ # title('Diff (Nearest Neighbour - Binary)');
+
+ # end
\ No newline at end of file
diff --git a/tests/test_pstd_elastic_3d_compare_binary_and_cuboid_sensor_mask.py b/tests/test_pstd_elastic_3d_compare_binary_and_cuboid_sensor_mask.py
new file mode 100644
index 000000000..ed54f0e09
--- /dev/null
+++ b/tests/test_pstd_elastic_3d_compare_binary_and_cuboid_sensor_mask.py
@@ -0,0 +1,231 @@
+"""
+Unit test to compare the simulation results using a binary and cuboid sensor mask
+"""
+
+import numpy as np
+from copy import deepcopy
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic3D import pstd_elastic_3d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.signals import tone_burst
+from kwave.reconstruction.beamform import focus
+
+def test_pstd_elastic_3d_compare_binary_and_cuboid_sensor_mask():
+
+ # set pass variable
+ test_pass: bool = True
+
+ # set additional literals to give further permutations of the test
+ COMPARISON_THRESH: float = 1e-13
+ PML_INSIDE: bool = True
+
+ # =========================================================================
+ # SIMULATION
+ # =========================================================================
+
+ # create the computational grid
+ Nx: int = 64 # number of grid points in the x direction
+ Ny: int = 64 # number of grid points in the y direction
+ Nz: int = 64 # number of grid points in the z direction
+ dx: float = 0.1e-3 # grid point spacing in the x direction [m]
+ dy: float = 0.1e-3 # grid point spacing in the y direction [m]
+ dz: float = 0.1e-3 # grid point spacing in the z direction [m]
+ kgrid = kWaveGrid(Vector([Nx, Ny, Nz]), Vector([dx, dy, dz]))
+
+ # define the properties of the upper layer of the propagation medium
+ sound_speed_compression = 1500.0 * np.ones((Nx, Ny, Nz)) # [m/s]
+ sound_speed_shear = np.zeros((Nx, Ny, Nz)) # [m/s]
+ density = 1000.0 * np.ones((Nx, Ny, Nz)) # [kg/m^3]
+
+ # define the properties of the lower layer of the propagation medium
+ sound_speed_compression[Nx // 2 - 1:, :, :] = 2000.0 # [m/s]
+ sound_speed_shear[Nx // 2 - 1:, :, :] = 800.0 # [m/s]
+ density[Nx // 2 - 1:, :, :] = 1200.0 # [kg/m^3]
+
+ medium = kWaveMedium(sound_speed=sound_speed_compression,
+ density=density,
+ sound_speed_shear=sound_speed_shear,
+ sound_speed_compression=sound_speed_compression)
+
+ # create the time array
+ cfl = 0.1
+ t_end = 5e-6
+ kgrid.makeTime(np.max(medium.sound_speed_compression), cfl, t_end)
+
+ source = kSource()
+
+ # define source mask to be a square piston
+ source_x_pos: int = 10 # [grid points]
+ source_radius: int = 15 # [grid points]
+ source.u_mask = np.zeros((Nx, Ny, Nz), dtype=bool)
+ source.u_mask[source_x_pos,
+ Ny // 2 - source_radius:Ny // 2 + source_radius,
+ Nz // 2 - source_radius:Nz // 2 + source_radius] = True
+
+ # define source to be a velocity source
+ source_freq = 2e6 # [Hz]
+ source_cycles = 3
+ source_mag = 1e-6
+ fs = 1.0 / kgrid.dt
+ source.ux = source_mag * tone_burst(fs, source_freq, source_cycles)
+
+ # set source focus
+ source.ux = focus(kgrid, deepcopy(source.ux), deepcopy(source.u_mask), Vector([0.0, 0.0, 0.0]), 1500.0)
+
+ # define list of cuboid corners using two intersecting cuboids
+ cuboid_corners = np.transpose(np.array([[20, 40, 30, 30, 50, 40],
+ [10, 35, 30, 25, 42, 40]], dtype=int)) - int(1)
+
+ # create sensor
+ sensor = kSensor()
+
+ #create sensor mask
+ sensor.mask = cuboid_corners
+
+ # set the variables to record
+ sensor.record = ['p', 'p_max', 'p_min', 'p_rms', 'p_max_all', 'p_min_all', 'p_final',
+ 'u', 'u_max', 'u_min', 'u_rms', 'u_max_all', 'u_min_all', 'u_final',
+ 'I', 'I_avg']
+
+ # run the simulation as normal
+ simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=PML_INSIDE,
+ kelvin_voigt_model=False)
+ # run the simulation
+ sensor_data_cuboids = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ medium=deepcopy(medium),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ simulation_options=deepcopy(simulation_options))
+
+ # create a binary mask for display from the list of corners
+ sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool)
+ cuboid_index: int = 0
+ sensor.mask[cuboid_corners[0, cuboid_index]:cuboid_corners[3, cuboid_index] + 1,
+ cuboid_corners[1, cuboid_index]:cuboid_corners[4, cuboid_index] + 1,
+ cuboid_corners[2, cuboid_index]:cuboid_corners[5, cuboid_index] + 1] = True
+
+ # run the simulation
+ sensor_data_comp1 = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ medium=deepcopy(medium),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ simulation_options=deepcopy(simulation_options))
+
+ # compute the error from the first cuboid
+ L_inf_p = np.max(np.abs(sensor_data_cuboids[cuboid_index].p -
+ np.reshape(sensor_data_comp1.p, np.shape(sensor_data_cuboids[cuboid_index].p), order='F') )) / np.max(np.abs(sensor_data_comp1.p))
+ # L_inf_p_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_max - sensor_data_comp1.p_max)) / np.max(np.abs(sensor_data_comp1.p_max))
+ # L_inf_p_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_min - sensor_data_comp1.p_min)) / np.max(np.abs(sensor_data_comp1.p_min))
+ # L_inf_p_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_rms - sensor_data_comp1.p_rms)) / np.max(np.abs(sensor_data_comp1.p_rms))
+
+ # L_inf_ux = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux - sensor_data_comp1.ux)) / np.max(np.abs(sensor_data_comp1.ux))
+ # L_inf_ux_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_max - sensor_data_comp1.ux_max)) / np.max(np.abs(sensor_data_comp1.ux_max))
+ # L_inf_ux_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_min - sensor_data_comp1.ux_min)) / np.max(np.abs(sensor_data_comp1.ux_min))
+ # L_inf_ux_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_rms - sensor_data_comp1.ux_rms)) / np.max(np.abs(sensor_data_comp1.ux_rms))
+
+ # L_inf_uy = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy - sensor_data_comp1.uy)) / np.max(np.abs(sensor_data_comp1.uy))
+ # L_inf_uy_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_max - sensor_data_comp1.uy_max)) / np.max(np.abs(sensor_data_comp1.uy_max))
+ # L_inf_uy_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_min - sensor_data_comp1.uy_min)) / np.max(np.abs(sensor_data_comp1.uy_min))
+ # L_inf_uy_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_rms - sensor_data_comp1.uy_rms)) / np.max(np.abs(sensor_data_comp1.uy_rms))
+
+ # L_inf_uz = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz - sensor_data_comp1.uz)) / np.max(np.abs(sensor_data_comp1.uz))
+ # L_inf_uz_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz_max - sensor_data_comp1.uz_max)) / np.max(np.abs(sensor_data_comp1.uz_max))
+ # L_inf_uz_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz_min - sensor_data_comp1.uz_min)) / np.max(np.abs(sensor_data_comp1.uz_min))
+ # L_inf_uz_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz_rms - sensor_data_comp1.uz_rms)) / np.max(np.abs(sensor_data_comp1.uz_rms))
+
+ # # compute the error from the total variables
+ # L_inf_p_max_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_max_all - sensor_data_comp1.p_max_all)) / np.max(np.abs(sensor_data_comp1.p_max_all))
+ # L_inf_ux_max_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_max_all - sensor_data_comp1.ux_max_all)) / np.max(np.abs(sensor_data_comp1.ux_max_all))
+ # L_inf_uy_max_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_max_all - sensor_data_comp1.uy_max_all)) / np.max(np.abs(sensor_data_comp1.uy_max_all))
+ # L_inf_uz_max_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz_max_all - sensor_data_comp1.uz_max_all)) / np.max(np.abs(sensor_data_comp1.uz_max_all))
+
+ # L_inf_p_min_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_min_all - sensor_data_comp1.p_min_all)) / np.max(np.abs(sensor_data_comp1.p_min_all))
+ # L_inf_ux_min_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_min_all - sensor_data_comp1.ux_min_all)) / np.max(np.abs(sensor_data_comp1.ux_min_all))
+ # L_inf_uy_min_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_min_all - sensor_data_comp1.uy_min_all)) / np.max(np.abs(sensor_data_comp1.uy_min_all))
+ # L_inf_uz_min_all = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz_min_all - sensor_data_comp1.uz_min_all)) / np.max(np.abs(sensor_data_comp1.uz_min_all))
+
+ # L_inf_p_final = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_final - sensor_data_comp1.p_final)) / np.max(np.abs(sensor_data_comp1.p_final))
+ # L_inf_ux_final = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_final - sensor_data_comp1.ux_final)) / np.max(np.abs(sensor_data_comp1.ux_final))
+ # L_inf_uy_final = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_final - sensor_data_comp1.uy_final)) / np.max(np.abs(sensor_data_comp1.uy_final))
+ # L_inf_uz_final = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz_final - sensor_data_comp1.uz_final)) / np.max(np.abs(sensor_data_comp1.uz_final))
+
+ # get maximum error
+ L_inf_max = np.max([L_inf_p, #L_inf_p_max, L_inf_p_min, L_inf_p_rms,
+ # L_inf_ux, L_inf_ux_max, L_inf_ux_min, L_inf_ux_rms,
+ # L_inf_uy, L_inf_uy_max, L_inf_uy_min, L_inf_uy_rms,
+ # L_inf_uz, L_inf_uz_max, L_inf_uz_min, L_inf_uz_rms,
+ # L_inf_p_max_all, L_inf_ux_max_all, L_inf_uy_max_all, L_inf_uz_max_all,
+ # L_inf_p_min_all, L_inf_ux_min_all, L_inf_uy_min_all, L_inf_uz_min_all,
+ # L_inf_p_final, L_inf_ux_final, L_inf_uy_final, L_inf_uz_final
+ ])
+
+ # compute pass
+ if (L_inf_max > COMPARISON_THRESH):
+ test_pass = False
+ msg: str = "fails on first cuboids: " + str(L_inf_max) + " > " + str(COMPARISON_THRESH)
+ else:
+ print("passses on first cuboids: " + str(L_inf_max) + " < " + str(COMPARISON_THRESH))
+
+ assert test_pass, msg
+
+ # create a binary mask for display from the list of corners
+
+ sensor.mask = np.zeros((Nx, Ny, Nz), dtype=bool)
+ cuboid_index: int = 1
+ sensor.mask[cuboid_corners[0, cuboid_index]:cuboid_corners[3, cuboid_index] + 1,
+ cuboid_corners[1, cuboid_index]:cuboid_corners[4, cuboid_index] + 1,
+ cuboid_corners[2, cuboid_index]:cuboid_corners[5, cuboid_index] + 1] = True
+
+ # run the simulation
+ sensor_data_comp2 = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ medium=deepcopy(medium),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ simulation_options=deepcopy(simulation_options))
+
+ # compute the error from the second cuboid
+ L_inf_p = np.max(np.abs(sensor_data_cuboids[cuboid_index].p -
+ np.reshape(sensor_data_comp2.p, np.shape(sensor_data_cuboids[cuboid_index].p), order='F') )) / np.max(np.abs(sensor_data_comp2.p))
+
+ L_inf_p_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_max -
+ np.reshape(sensor_data_comp2.p_max, np.shape(sensor_data_cuboids[cuboid_index].p_max), order='F') )) / np.max(np.abs(sensor_data_comp2.p_max))
+
+ # L_inf_p_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_min - sensor_data_comp2.p_min)) / np.max(np.abs(sensor_data_comp2.p_min))
+ # L_inf_p_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].p_rms - sensor_data_comp2.p_rms)) / np.max(np.abs(sensor_data_comp2.p_rms))
+
+ # L_inf_ux = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux - sensor_data_comp2.ux)) / np.max(np.abs(sensor_data_comp2.ux))
+ # L_inf_ux_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_max - sensor_data_comp2.ux_max)) / np.max(np.abs(sensor_data_comp2.ux_max))
+ # L_inf_ux_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_min - sensor_data_comp2.ux_min)) / np.max(np.abs(sensor_data_comp2.ux_min))
+ # L_inf_ux_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].ux_rms - sensor_data_comp2.ux_rms)) / np.max(np.abs(sensor_data_comp2.ux_rms))
+
+ # L_inf_uy = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy - sensor_data_comp2.uy)) / np.max(np.abs(sensor_data_comp2.uy))
+ # L_inf_uy_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_max - sensor_data_comp2.uy_max)) / np.max(np.abs(sensor_data_comp2.uy_max))
+ # L_inf_uy_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_min - sensor_data_comp2.uy_min)) / np.max(np.abs(sensor_data_comp2.uy_min))
+ # L_inf_uy_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].uy_rms - sensor_data_comp2.uy_rms)) / np.max(np.abs(sensor_data_comp2.uy_rms))
+
+ # L_inf_uz = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz - sensor_data_comp2.uz)) / np.max(np.abs(sensor_data_comp2.uz))
+ # L_inf_uz_max = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz_max - sensor_data_comp2.uz_max)) / np.max(np.abs(sensor_data_comp2.uz_max))
+ # L_inf_uz_min = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz_min - sensor_data_comp2.uz_min)) / np.max(np.abs(sensor_data_comp2.uz_min))
+ # L_inf_uz_rms = np.max(np.abs(sensor_data_cuboids[cuboid_index].uz_rms - sensor_data_comp2.uz_rms)) / np.max(np.abs(sensor_data_comp2.uz_rms))
+
+ # get maximum error
+ L_inf_max = np.max([L_inf_p, L_inf_p_max, #L_inf_p_min, L_inf_p_rms,
+ # L_inf_ux, L_inf_ux_max, L_inf_ux_min, L_inf_ux_rms,
+ # L_inf_uy, L_inf_uy_max, L_inf_uy_min, L_inf_uy_rms,
+ # L_inf_uz, L_inf_uz_max, L_inf_uz_min, L_inf_uz_rms
+ ])
+
+ # compute pass
+ if (L_inf_max > COMPARISON_THRESH):
+ test_pass = False
+ msg: str = "fails on second cuboids: " + str(L_inf_max) + " > " + str(COMPARISON_THRESH)
+ else:
+ print("passses on second cuboids: " + str(L_inf_max) + " < " + str(COMPARISON_THRESH))
+
+ assert test_pass, msg
diff --git a/tests/test_pstd_elastic_3d_compare_labelled_and_binary_source_mask.py b/tests/test_pstd_elastic_3d_compare_labelled_and_binary_source_mask.py
new file mode 100644
index 000000000..2fcf46e74
--- /dev/null
+++ b/tests/test_pstd_elastic_3d_compare_labelled_and_binary_source_mask.py
@@ -0,0 +1,197 @@
+"""
+Unit test to compare the simulation results using a labelled and binary source mask.
+"""
+
+import numpy as np
+from copy import deepcopy
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic3D import pstd_elastic_3d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.mapgen import make_multi_bowl
+from kwave.utils.filters import filter_time_series
+
+def test_pstd_elastic_3d_compare_labelled_and_binary_source_mask():
+
+ # set pass variable
+ test_pass: bool = True
+
+ # set additional literals to give further permutations of the test.
+ COMPARISON_THRESH: float = 1e-14
+ pml_inside: bool = True
+
+ # =========================================================================
+ # SIMULATION
+ # =========================================================================
+
+ # create the computational grid
+ Nx: int = 64 # number of grid points in the x direction
+ Ny: int = 64 # number of grid points in the y direction
+ Nz: int = 64 # number of grid points in the z direction
+ dx: float = 0.1e-3 # grid point spacing in the x direction [m]
+ dy: float = 0.1e-3 # grid point spacing in the y direction [m]
+ dz: float = 0.1e-3 # grid point spacing in the z direction [m]
+ kgrid = kWaveGrid(Vector([Nx, Ny, Nz]), Vector([dx, dy, dz]))
+
+ # define the properties of the propagation medium
+ sound_speed_compression = 1500.0 # [m/s]
+ sound_speed_shear = 1000.0 # [m/s]
+ density = 1000.0 # [kg/m^3]
+ medium = kWaveMedium(sound_speed_compression,
+ density=density,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear)
+
+ # create the time array using default CFL condition
+ t_end: float = 3e-6
+ kgrid.makeTime(medium.sound_speed_compression, t_end=t_end)
+
+ # define multiple curved transducer elements
+ bowl_pos = np.array([(19.0, 19.0, Nz / 2.0 - 1.0), (48.0, 48.0, Nz / 2.0 - 1.0)])
+ bowl_radius = np.array([20.0, 15.0])
+ bowl_diameter = np.array([int(15), int(21)], dtype=np.uint8)
+ bowl_focus = np.array([(int(31), int(31), int(31))], dtype=np.uint8)
+
+ binary_mask, labelled_mask = make_multi_bowl(Vector([Nx, Ny, Nz]), bowl_pos, bowl_radius, bowl_diameter, bowl_focus)
+
+ # create sensor object
+ sensor = kSensor()
+
+ # create a sensor mask covering the entire computational domain using the
+ # opposing corners of a cuboid. These means cuboid corners will be used
+ sensor.mask = np.array([[0, 0, 0, Nx - 1, Ny - 1, Nz - 1]], dtype=int).T
+
+ # set the record mode capture the final wave-field and the statistics at
+ # each sensor point
+ sensor.record = ['p_final', 'p_max']
+
+ # assign the input options
+ simulation_options = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_inside=pml_inside)
+
+ # define a time varying sinusoidal source
+ source_freq_0 = 1e6 # [Hz]
+ source_mag_0 = 0.5 # [Pa]
+ source_0 = source_mag_0 * np.sin(2.0 * np.pi * source_freq_0 * np.squeeze(kgrid.t_array))
+ source_0 = filter_time_series(kgrid, medium, deepcopy(source_0))
+
+ source_freq_1 = 3e6 # [Hz]
+ source_mag_1 = 0.8 # [Pa]
+ source_1 = source_mag_1 * np.sin(2.0 * np.pi * source_freq_1 * np.squeeze(kgrid.t_array))
+ source_1 = filter_time_series(kgrid, medium, deepcopy(source_1))
+
+ # assemble sources
+ labelled_sources = np.zeros((2, kgrid.Nt))
+ labelled_sources[0, :] = np.squeeze(source_0)
+ labelled_sources[1, :] = np.squeeze(source_1)
+
+ # create ksource object
+ source = kSource()
+
+ # source mask is from the labelled mask
+ source.s_mask = deepcopy(labelled_mask)
+
+ # assign sources from labelled source
+ source.sxx = deepcopy(labelled_sources)
+ source.syy = deepcopy(labelled_sources)
+ source.szz = deepcopy(labelled_sources)
+ source.sxy = deepcopy(labelled_sources)
+ source.sxz = deepcopy(labelled_sources)
+ source.syz = deepcopy(labelled_sources)
+
+ # run the simulation using the labelled source mask
+ sensor_data_labelled_s = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+ # assign the source using a binary source mask
+ del source
+ source = kSource()
+
+ # source mask is from **binary source mask**
+ source.s_mask = binary_mask
+
+ index_mask = labelled_mask.flatten('F')[labelled_mask.flatten('F') != 0].astype(int) - int(1)
+
+ source.sxx = deepcopy(labelled_sources[index_mask, :])
+ source.syy = deepcopy(source.sxx)
+ source.szz = deepcopy(source.sxx)
+ source.sxy = deepcopy(source.sxx)
+ source.sxz = deepcopy(source.sxx)
+ source.syz = deepcopy(source.sxx)
+
+ # run the simulation using the a binary source mask
+ sensor_data_binary_s = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+ # compute the error from the first cuboid
+ L_inf_final_stress_s = np.max(np.abs(sensor_data_labelled_s[1].p_final - sensor_data_binary_s[1].p_final)) / np.max(np.abs(sensor_data_binary_s[1].p_final))
+ L_inf_max_stress_s = np.max(np.abs(sensor_data_labelled_s[0].p_max - sensor_data_binary_s[0].p_max)) / np.max(np.abs(sensor_data_binary_s[0].p_max))
+
+ # ----------------------------------------
+ # repeat for a velocity source
+ # ----------------------------------------
+
+ del source
+ source = kSource()
+
+ # assign the source using a **labelled** source mask
+ source.u_mask = deepcopy(labelled_mask)
+ source.ux = 1e-6 * deepcopy(labelled_sources)
+ source.uy = 1e-6 * deepcopy(labelled_sources)
+ source.uz = 1e-6 * deepcopy(labelled_sources)
+
+ # run the simulation using the labelled source mask
+ sensor_data_labelled_v = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+ # assign the source using a **binary** source mask
+ del source
+ source = kSource()
+ source.u_mask = binary_mask
+ index_mask = labelled_mask.flatten('F')[labelled_mask.flatten('F') != 0].astype(int) - int(1)
+ source.ux = 1e-6 * labelled_sources[index_mask, :]
+ source.uy = deepcopy(source.ux)
+ source.uz = deepcopy(source.ux)
+
+ # run the simulation using the a binary source mask
+ sensor_data_binary_v = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options))
+
+ # compute the error from the first cuboid
+ L_inf_final_v = np.max(np.abs(sensor_data_labelled_v[1].p_final - sensor_data_binary_v[1].p_final)) / np.max(np.abs(sensor_data_binary_v[1].p_final))
+ L_inf_max_v = np.max(np.abs(sensor_data_labelled_v[0].p_max - sensor_data_binary_v[0].p_max)) / np.max(np.abs(sensor_data_binary_v[0].p_max))
+
+ # compute pass
+ if (L_inf_max_stress_s > COMPARISON_THRESH):
+ test_pass = False
+ assert test_pass, "cuboid to binary sensor mask using a stress source " + str(L_inf_max_stress_s)
+
+ # compute pass
+ if (L_inf_final_stress_s > COMPARISON_THRESH):
+ test_pass = False
+ assert test_pass, "cuboid to binary sensor mask using a stress source " + str(L_inf_final_stress_s)
+
+ # compute pass
+ if (L_inf_final_v > COMPARISON_THRESH):
+ test_pass = False
+ assert test_pass, "cuboid to binary sensor mask using a velocity source " + str(L_inf_final_v)
+
+ if (L_inf_max_v > COMPARISON_THRESH):
+ test_pass = False
+ assert test_pass, "cuboid to binary sensor mask using a velocity source " + str(L_inf_max_v)
diff --git a/tests/test_pstd_elastic_3d_compare_with_kspaceFirstOrder3D.py b/tests/test_pstd_elastic_3d_compare_with_kspaceFirstOrder3D.py
new file mode 100644
index 000000000..42d96dc89
--- /dev/null
+++ b/tests/test_pstd_elastic_3d_compare_with_kspaceFirstOrder3D.py
@@ -0,0 +1,362 @@
+"""
+Unit test to compare that the elastic code with the shear wave speed set to
+zero gives the same answers as the regular fluid code in k-Wave.
+"""
+
+import numpy as np
+from copy import deepcopy
+import matplotlib.pyplot as plt
+#import pytest
+
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic3D import pstd_elastic_3d
+from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.options.simulation_execution_options import SimulationExecutionOptions
+from kwave.utils.filters import filter_time_series
+from kwave.utils.mapgen import make_ball
+
+#@pytest.mark.skip(reason="not ready")
+def test_pstd_elastic_3D_compare_with_kspaceFirstOrder3D():
+
+ # set additional literals to give further permutations of the test
+ HETEROGENEOUS: bool = True
+ USE_PML: bool = True
+ DATA_CAST: str = 'on'
+ COMPARISON_THRESH: float = 5e-10
+
+ # option to skip the first point in the time series (for p0 sources, there
+ # is a strange bug where there is a high error for the first stored time
+ # point)
+ COMP_START_INDEX: int = 1
+
+ # =========================================================================
+ # SIMULATION
+ # =========================================================================
+
+ # create the computational grid
+ Nx: int = 64
+ Ny: int = 62
+ Nz: int = 60
+ dx: float = 0.1e-3
+ dy: float = 0.1e-3
+ dz: float = 0.1e-3
+ kgrid = kWaveGrid(Vector([Nx, Ny, Nz]), Vector([dx, dy, dz]))
+
+ # define the medium properties
+ cp: float = 1500.0
+ cs: float = 0.0
+ rho: float = 1000.0
+
+ # create the time zarray
+ CFL: float = 0.1
+ t_end: float = 3e-6
+ kgrid.makeTime(cp, CFL, t_end)
+
+ # create and assign the variables
+ if HETEROGENEOUS:
+ # elastic medium
+ sound_speed_compression = cp * np.ones((Nx, Ny, Nz))
+ sound_speed_compression[Nx // 2 - 1:, :, :] = 2.0 * cp
+ sound_speed_shear = cs * np.ones((Nx, Ny, Nz))
+ density = rho * np.ones((Nx, Ny, Nz))
+ medium_elastic = kWaveMedium(sound_speed=sound_speed_compression,
+ density=density,
+ sound_speed_compression=sound_speed_compression,
+ sound_speed_shear=sound_speed_shear)
+ # fluid medium
+ sound_speed = cp * np.ones((Nx, Ny, Nz))
+ sound_speed[Nx // 2 - 1:, :, :] = 2.0 * cp
+ density = rho * np.ones((Nx, Ny, Nz))
+ medium_fluid = kWaveMedium(sound_speed, density=density)
+ else:
+ # elastic medium
+ medium_elastic = kWaveMedium(cp,
+ density=rho,
+ sound_speed_compression=cp,
+ sound_speed_shear=cs)
+ # fluid medium
+ medium_fluid = kWaveMedium(sound_speed=cp,
+ density=rho)
+
+
+ # set pass variable
+ test_pass: bool = True
+
+ # test names
+ test_names = ['source.p0',
+ 'source.p, additive',
+ 'source.p, dirichlet', # gives warning
+ 'source.ux, additive',
+ #'source.ux, dirichlet',
+ #'source.uy, additive',
+ #'source.uy, dirichlet',
+ #'source.uz, additive',
+ #'source.uz, dirichlet'
+ ]
+
+ # define a single point sensor
+ sensor_elastic = kSensor()
+ sensor_fluid = kSensor()
+ sensor_elastic.mask = np.zeros((Nx, Ny, Nz), dtype=bool)
+ sensor_fluid.mask = np.zeros((Nx, Ny, Nz), dtype=bool)
+ sensor_elastic.mask[3 * Nx // 4 - 1, 3 * Ny // 4 - 1, 3 * Nz // 4 - 1] = True
+ sensor_fluid.mask[3 * Nx // 4 - 1, 3 * Ny // 4 - 1, 3 * Nz // 4 - 1] = True
+
+ # set some things to record
+ sensor_elastic.record = ['p', 'p_final', 'u', 'u_final']
+ sensor_fluid.record = ['p', 'p_final', 'u', 'u_final']
+
+
+ # loop through tests
+ for test_num, test_name in enumerate(test_names):
+
+ # update command line
+ print('Running Number: ', test_num, ':', test_name)
+
+ # set up sources
+ source_fluid = kSource()
+ source_elastic = kSource()
+
+ if test_name == 'source.p0':
+ # create initial pressure distribution using makeBall
+ disc_magnitude: float = 5.0 # [Pa]
+ disc_x_pos: int = Nx // 2 - 11 # [grid points]
+ disc_y_pos: int = Ny // 2 - 1 # [grid points]
+ disx_z_pos: int = Nz // 2 - 1 # [grid points]
+ disc_radius: int = 3 # [grid points]
+ source_fluid.p0 = disc_magnitude * make_ball(Vector([Nx, Ny, Nz]),
+ Vector([disc_x_pos, disc_y_pos, disx_z_pos]),
+ disc_radius)
+ disc_magnitude: float = 5.0 # [Pa]
+ disc_x_pos: int = Nx // 2 - 11 # [grid points]
+ disc_y_pos: int = Ny // 2 - 1 # [grid points]
+ disx_z_pos: int = Nz // 2 - 1 # [grid points]
+ disc_radius: int = 3 # [grid points]
+ source_elastic.p0 = disc_magnitude * make_ball(Vector([Nx, Ny, Nz]),
+ Vector([disc_x_pos, disc_y_pos, disx_z_pos]),
+ disc_radius)
+
+ elif test_name == 'source.p, additive' or test_name == 'source.p, dirichlet':
+ # create pressure source
+ source_fluid.p_mask = np.zeros((Nx, Ny, Nz))
+ source_fluid.p_mask[Nx // 2 - 11, Ny // 2 - 1, Nz // 2 - 1] = 1
+ source_fluid.p = 5.0 * np.sin(2.0 * np.pi * 1e6 * np.squeeze(kgrid.t_array))
+ source_fluid.p = filter_time_series(deepcopy(kgrid),
+ deepcopy(medium_fluid),
+ deepcopy(source_fluid.p))
+ # create equivalent elastic source
+ source_elastic.s_mask = deepcopy(source_fluid.p_mask)
+ source_elastic.sxx = deepcopy(-source_fluid.p)
+ source_elastic.syy = deepcopy(-source_fluid.p)
+ source_elastic.szz = deepcopy(-source_fluid.p)
+
+ elif test_name == 'source.ux, additive' or test_name == 'source.ux, dirichlet':
+ # create velocity source
+ source_fluid.u_mask = np.zeros((Nx, Ny, Nz))
+ source_fluid.u_mask[Nx // 2 - 11, Ny // 2 - 1, Nz // 2 - 1] = 1
+ source_fluid.ux = 5.0 * np.sin(2.0 * np.pi * 1e6 * np.squeeze(kgrid.t_array)) / (cp * rho)
+ source_fluid.ux = filter_time_series(kgrid, medium_fluid, deepcopy(source_fluid.ux))
+ # create equivalent elastic source
+ source_elastic.u_mask = np.zeros((Nx, Ny, Nz))
+ source_elastic.u_mask[Nx // 2 - 11, Ny // 2 - 1, Nz // 2 - 1] = 1
+ source_elastic.ux = 5.0 * np.sin(2.0 * np.pi * 1e6 * np.squeeze(kgrid.t_array)) / (cp * rho)
+ source_elastic.ux = filter_time_series(kgrid, medium_fluid, deepcopy(source_fluid.ux))
+ # source_elastic = deepcopy(source_fluid)
+
+ elif test_name == 'source.uy, additive' or test_name == 'source.uy, dirichlet':
+ # create velocity source
+ source_fluid.u_mask = np.zeros((Nx, Ny, Nz))
+ source_fluid.u_mask[Nx // 2 - 11, Ny // 2 - 1, Nz // 2 - 1] = 1
+ source_fluid.uy = 5.0 * np.sin(2.0 * np.pi * 1e6 * np.squeeze(kgrid.t_array)) / (cp * rho)
+ source_fluid.uy = filter_time_series(kgrid, medium_fluid, deepcopy(source_fluid.uy))
+ # create equivalent elastic source
+ source_elastic = deepcopy(source_fluid)
+
+ elif test_name == 'source.uz, additive' or test_name == 'source.uz, dirichlet':
+ # create velocity source
+ source_fluid.u_mask = np.zeros((Nx, Ny, Nz))
+ source_fluid.u_mask[Nx // 2 - 11, Ny // 2 - 1, Nz // 2 - 1] = 1
+ source_fluid.uz = 5.0 * np.sin(2.0 * np.pi * 1e6 * np.squeeze(kgrid.t_array)) / (cp * rho)
+ source_fluid.uz = filter_time_series(kgrid, medium_fluid, deepcopy(source_fluid.uz))
+ # create equivalent elastic source
+ source_elastic = deepcopy(source_fluid)
+
+ # set source mode
+ if test_name == 'source.p, additive':
+ source_fluid.p_mode = 'additive'
+ source_elastic.s_mode = 'additive'
+ elif test_name == 'source.p, dirichlet':
+ source_fluid.p_mode = 'dirichlet'
+ source_elastic.s_mode = 'dirichlet'
+ elif test_name == 'source.ux, additive' or test_name == 'source.uy, additive' or test_name == 'source.uz, additive':
+ source_fluid.u_mode = 'additive'
+ source_elastic.u_mode = 'additive'
+ elif test_name == 'source.ux, dirichlet' or test_name == 'source.uy, dirichlet' or test_name == 'source.uz, dirichlet':
+ source_fluid.u_mode = 'dirichlet'
+ source_elastic.u_mode = 'dirichlet'
+
+ # options for writing to file, but not doing simulations
+ input_filename_p = 'data_p_input.h5'
+ output_filename_p = 'data_p_output.h5'
+ DATA_CAST: str = 'single'
+ DATA_PATH = '.'
+
+ # set input args
+ if not USE_PML:
+ simulation_options_fluid = SimulationOptions(data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename_p,
+ output_filename=output_filename_p,
+ data_path=DATA_PATH,
+ use_kspace=False,
+ pml_alpha=0.0,
+ hdf_compression_level='lzf')
+ simulation_options_elastic = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_alpha=0.0,
+ kelvin_voigt_model=False)
+ else:
+ simulation_options_fluid = SimulationOptions(data_cast=DATA_CAST,
+ data_recast=True,
+ save_to_disk=True,
+ input_filename=input_filename_p,
+ output_filename=output_filename_p,
+ data_path=DATA_PATH,
+ use_kspace=False,
+ hdf_compression_level='lzf')
+ simulation_options_elastic = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ kelvin_voigt_model=False)
+
+ # options for executing simulations
+ execution_options_fluid = SimulationExecutionOptions(is_gpu_simulation=True, delete_data=False)
+
+ # run the fluid simulation
+ sensor_data_fluid = kspaceFirstOrder3D(kgrid=deepcopy(kgrid),
+ source=deepcopy(source_fluid),
+ sensor=deepcopy(sensor_fluid),
+ medium=deepcopy(medium_fluid),
+ simulation_options=deepcopy(simulation_options_fluid),
+ execution_options=deepcopy(execution_options_fluid))
+
+ # run the elastic simulation
+ sensor_data_elastic = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source_elastic),
+ sensor=deepcopy(sensor_elastic),
+ medium=deepcopy(medium_elastic),
+ simulation_options=deepcopy(simulation_options_elastic))
+
+ # reshape data to fit
+ sensor_data_elastic['p_final'] = np.transpose(sensor_data_elastic['p_final'], (2, 1, 0))
+ sensor_data_elastic['p_final'] = sensor_data_elastic['p_final'].reshape(sensor_data_elastic['p_final'].shape, order='F')
+
+ sensor_data_elastic['ux_final'] = np.transpose(sensor_data_elastic['ux_final'], (2, 1, 0))
+ sensor_data_elastic['ux_final'] = sensor_data_elastic['ux_final'].reshape(sensor_data_elastic['ux_final'].shape, order='F')
+
+ sensor_data_elastic['uy_final'] = np.transpose(sensor_data_elastic['uy_final'], (2, 1, 0))
+ sensor_data_elastic['uy_final'] = sensor_data_elastic['uy_final'].reshape(sensor_data_elastic['uy_final'].shape, order='F')
+
+ sensor_data_elastic['uz_final'] = np.transpose(sensor_data_elastic['uz_final'], (2, 1, 0))
+ sensor_data_elastic['uz_final'] = sensor_data_elastic['uz_final'].reshape(sensor_data_elastic['uz_final'].shape, order='F')
+
+ # compute comparisons for time series
+ L_inf_p = np.max(np.abs(np.squeeze(sensor_data_elastic['p'])[COMP_START_INDEX:] - sensor_data_fluid['p'][COMP_START_INDEX:])) / np.max(np.abs(sensor_data_fluid['p'][COMP_START_INDEX:]))
+ L_inf_ux = np.max(np.abs(np.squeeze(sensor_data_elastic['ux'])[COMP_START_INDEX:] - sensor_data_fluid['ux'][COMP_START_INDEX:])) / np.max(np.abs(sensor_data_fluid['ux'][COMP_START_INDEX:]))
+ L_inf_uy = np.max(np.abs(np.squeeze(sensor_data_elastic['uy'])[COMP_START_INDEX:] - sensor_data_fluid['uy'][COMP_START_INDEX:])) / np.max(np.abs(sensor_data_fluid['uy'][COMP_START_INDEX:]))
+ L_inf_uz = np.max(np.abs(np.squeeze(sensor_data_elastic['uz'])[COMP_START_INDEX:] - sensor_data_fluid['uz'][COMP_START_INDEX:])) / np.max(np.abs(sensor_data_fluid['uz'][COMP_START_INDEX:]))
+
+ # compuate comparisons for field
+ L_inf_p_final = np.max(np.abs(sensor_data_elastic['p_final'] - sensor_data_fluid['p_final'])) / np.max(np.abs(sensor_data_fluid['p_final']))
+ L_inf_ux_final = np.max(np.abs(sensor_data_elastic['ux_final'] - sensor_data_fluid['ux_final'])) / np.max(np.abs(sensor_data_fluid['ux_final']))
+ L_inf_uy_final = np.max(np.abs(sensor_data_elastic['uy_final'] - sensor_data_fluid['uy_final'])) / np.max(np.abs(sensor_data_fluid['uy_final']))
+ L_inf_uz_final = np.max(np.abs(sensor_data_elastic['uz_final'] - sensor_data_fluid['uz_final'])) / np.max(np.abs(sensor_data_fluid['uz_final']))
+
+ # compute pass
+ latest_test: bool = False
+ if ((L_inf_p < COMPARISON_THRESH) and
+ (L_inf_ux < COMPARISON_THRESH) and
+ (L_inf_uy < COMPARISON_THRESH) and
+ (L_inf_uz < COMPARISON_THRESH) and
+ (L_inf_p_final < COMPARISON_THRESH) and
+ (L_inf_ux_final < COMPARISON_THRESH) and
+ (L_inf_uy_final < COMPARISON_THRESH) and
+ (L_inf_uz_final < COMPARISON_THRESH)):
+ # set test variable
+ latest_test = True
+ else:
+ print('fails')
+
+ if (L_inf_p < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_p =', L_inf_p)
+
+ if (L_inf_ux < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_ux =', L_inf_ux)
+
+ if (L_inf_uy < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_uy =', L_inf_uy)
+
+ if (L_inf_uz < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_uz =', L_inf_uz)
+
+ if (L_inf_p_final < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_p_final =', L_inf_p_final)
+
+ if (L_inf_ux_final < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_ux_final =', L_inf_ux_final)
+
+ if (L_inf_uy_final < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_uy_final =', L_inf_uy_final)
+
+ if (L_inf_uz_final < COMPARISON_THRESH):
+ latest_test = True
+ else:
+ print('\tfails at L_inf_uz_final =', L_inf_uz_final)
+
+ test_pass = test_pass and latest_test
+
+
+ fig1, ((ax1a, ax1b), (ax1c, ax1d)) = plt.subplots(2, 2)
+ fig1.suptitle(f"{test_name}: Comparisons")
+ ax1a.plot(np.squeeze(sensor_data_elastic['p'])[COMP_START_INDEX:], 'r-o', sensor_data_fluid['p'][COMP_START_INDEX:], 'b--*')
+ ax1b.plot(np.squeeze(sensor_data_elastic['ux'])[COMP_START_INDEX:], 'r-o', sensor_data_fluid['ux'][COMP_START_INDEX:], 'b--*')
+ ax1c.plot(np.squeeze(sensor_data_elastic['uy'])[COMP_START_INDEX:], 'r-o', sensor_data_fluid['uy'][COMP_START_INDEX:], 'b--*')
+ ax1d.plot(np.squeeze(sensor_data_elastic['uz'])[COMP_START_INDEX:], 'r-o', sensor_data_fluid['uz'][COMP_START_INDEX:], 'b--*')
+
+ fig2, ((ax2a, ax2b), (ax2c, ax2d)) = plt.subplots(2, 2)
+ fig2.suptitle(f"{test_name}: Errors")
+ ax2a.plot(np.abs(np.squeeze(sensor_data_elastic['p'])[COMP_START_INDEX:] - sensor_data_fluid['p'][COMP_START_INDEX:]))
+ ax2b.plot(np.abs(np.squeeze(sensor_data_elastic['ux'])[COMP_START_INDEX:] - sensor_data_fluid['ux'][COMP_START_INDEX:]))
+ ax2c.plot(np.abs(np.squeeze(sensor_data_elastic['uy'])[COMP_START_INDEX:] - sensor_data_fluid['uy'][COMP_START_INDEX:]))
+ ax2d.plot(np.abs(np.squeeze(sensor_data_elastic['uz'])[COMP_START_INDEX:] - sensor_data_fluid['uz'][COMP_START_INDEX:]))
+
+
+
+ # clear structures
+ del source_fluid
+ del source_elastic
+ del sensor_data_elastic
+ del sensor_data_fluid
+
+ plt.show()
+
+ assert test_pass, "not working"
+
diff --git a/tests/test_pstd_elastic_3d_compare_with_pstd_elastic_2d.py b/tests/test_pstd_elastic_3d_compare_with_pstd_elastic_2d.py
new file mode 100644
index 000000000..cecf29350
--- /dev/null
+++ b/tests/test_pstd_elastic_3d_compare_with_pstd_elastic_2d.py
@@ -0,0 +1,709 @@
+"""
+# Unit test to compare an infinite line source in 2D and 3D in an
+# elastic medium to catch any coding bugs between the pstdElastic2D and
+# pstdElastic3D. 20 tests are performed:
+#
+# 1. lossless + source.p0 + homogeneous
+# 2. lossless + source.p0 + heterogeneous
+# 3. lossless + source.s (additive) + homogeneous
+# 4. lossless + source.s (additive) + heterogeneous
+# 5. lossless + source.s (dirichlet) + homogeneous
+# 6. lossless + source.s (dirichlet) + heterogeneous
+# 7. lossless + source.u (additive) + homogeneous
+# 8. lossless + source.u (additive) + heterogeneous
+# 9. lossless + source.u (dirichlet) + homogeneous
+# 10. lossless + source.u (dirichlet) + heterogeneous
+# 11. lossy + source.p0 + homogeneous
+# 12. lossy + source.p0 + heterogeneous
+# 13. lossy + source.s (additive) + homogeneous
+# 14. lossy + source.s (additive) + heterogeneous
+# 15. lossy + source.s (dirichlet) + homogeneous
+# 16. lossy + source.s (dirichlet) + heterogeneous
+# 17. lossy + source.u (additive) + homogeneous
+# 18. lossy + source.u (additive) + heterogeneous
+# 19. lossy + source.u (dirichlet) + homogeneous
+# 20. lossy + source.u (dirichlet) + heterogeneous
+#
+# For each test, the infinite line source in 3D is aligned in all three
+# directions.
+"""
+
+import numpy as np
+from copy import deepcopy
+import matplotlib.pyplot as plt
+# import pytest
+
+# from scipy.io import loadmat
+
+from kwave.data import Vector
+from kwave.kgrid import kWaveGrid
+from kwave.kmedium import kWaveMedium
+from kwave.ksource import kSource
+from kwave.pstdElastic2D import pstd_elastic_2d
+from kwave.pstdElastic3D import pstd_elastic_3d
+from kwave.ksensor import kSensor
+from kwave.options.simulation_options import SimulationOptions, SimulationType
+from kwave.utils.mapgen import make_circle
+from kwave.utils.matlab import rem
+from kwave.utils.filters import smooth
+
+
+
+def setMaterialProperties(medium: kWaveMedium, N1: int, N2: int, N3: int,
+ direction: int, interface_position: int,
+ cp1: float=1500.0, cs1: float=0.0, rho1: float=1000.0,
+ alpha_p1: float=0.5, alpha_s1: float=0.5):
+
+ # sound speed and density
+ medium.sound_speed_compression = cp1 * np.ones((N1, N2, N3), dtype=float)
+ medium.sound_speed_shear = cs1 * np.ones((N1, N2, N3), dtype=float)
+ medium.density = rho1 * np.ones((N1, N2, N3), dtype=float)
+
+ cp2: float = 2000.0
+ cs2: float = 800.0
+ rho2: float = 1200.0
+ alpha_p2: float = 1.0
+ alpha_s2: float = 1.0
+
+ # position of the heterogeneous interface
+ if direction == 1:
+ medium.sound_speed_compression[interface_position:, :, :] = cp2
+ medium.sound_speed_shear[interface_position:, :, :] = cs2
+ medium.density[interface_position:, :, :] = rho2
+ elif direction == 2:
+ medium.sound_speed_compression[:, interface_position:, :] = cp2
+ medium.sound_speed_shear[:, interface_position:, :] = cs2
+ medium.density[:, interface_position:, :] = rho2
+
+ # compress, so 2D simulations on 2d domain
+ medium.sound_speed_compression = np.squeeze(medium.sound_speed_compression)
+ medium.sound_speed_shear = np.squeeze(medium.sound_speed_shear)
+ medium.density = np.squeeze(medium.density)
+
+ medium.sound_speed = medium.sound_speed_compression
+
+ # absorption
+ if hasattr(medium, 'alpha_coeff_compression'):
+ if medium.alpha_coeff_compression is not None or medium.alpha_coeff_shear is not None:
+ medium.alpha_coeff_compression = alpha_p1 * np.ones((N1, N2, N3), dtype=np.float32)
+ medium.alpha_coeff_shear = alpha_s1 * np.ones((N1, N2, N3), dtype=np.float32)
+ if direction == 1:
+ medium.alpha_coeff_compression[interface_position:, :, :] = alpha_p2
+ medium.alpha_coeff_shear[interface_position:, :, :] = alpha_s2
+ elif direction == 2:
+ medium.alpha_coeff_compression[:, interface_position:, :] = alpha_p2
+ medium.alpha_coeff_shear[:, interface_position:, :] = alpha_s2
+ # compress, so 2D simulations on 2d domain
+ medium.alpha_coeff_compression = np.squeeze(medium.alpha_coeff_compression)
+ medium.alpha_coeff_shear = np.squeeze(medium.alpha_coeff_shear)
+ else:
+ pass
+
+
+# @pytest.mark.skip(reason="not ready")
+def test_pstd_elastic_3d_compare_with_pstd_elastic_2d():
+
+ verbose: bool = True
+
+ # set additional literals to give further permutations of the test
+ USE_PML = True
+ COMPARISON_THRESH = 1e-10
+ # this smooths everything not just p0
+ SMOOTH_P0_SOURCE = True
+ # USE_SG = True
+
+ # =========================================================================
+ # SIMULATION PARAMETERS
+ # =========================================================================
+
+ # define grid size
+ Nx: int = 64
+ Ny: int = 64
+ Nz: int = 32
+ dx: float = 0.1e-3
+ dy: float = 0.1e-3
+ dz: float = 0.1e-3
+
+ # define PML properties
+ pml_size: int = 10
+ if USE_PML:
+ pml_alpha: float = 2.0
+ else:
+ pml_alpha: float = 0.0
+
+ # define material properties
+ cp1: float = 1500.0
+ cs1: float = 0.0
+ rho1: float = 1000.0
+ alpha_p1: float = 0.5
+ alpha_s1: float = 0.5
+
+ # geometry
+ interface_position: int = Nx // 2 - 1
+
+ # set pass variable
+ test_pass: bool = True
+ all_tests: bool = True
+
+ # test names
+ test_names = [
+ 'lossless + source.p0 + homogeneous', #0
+ 'lossless + source.p0 + heterogeneous', #1
+ 'lossless + source.s (additive) + homogeneous', #2
+ 'lossless + source.s (additive) + heterogeneous', #3
+ 'lossless + source.s (dirichlet) + homogeneous', #4
+ 'lossless + source.s (dirichlet) + heterogeneous', #5
+ 'lossless + source.u (additive) + homogeneous', #6
+ 'lossless + source.u (additive) + heterogeneous', #7
+ 'lossless + source.u (dirichlet) + homogeneous', #8
+ 'lossless + source.u (dirichlet) + heterogeneous', #9
+ 'lossy + source.p0 + homogeneous', #10
+ 'lossy + source.p0 + heterogeneous', #11
+ 'lossy + source.s (additive) + homogeneous', #12
+ 'lossy + source.s (additive) + heterogeneous', #13
+ 'lossy + source.s (dirichlet) + homogeneous', #14
+ 'lossy + source.s (dirichlet) + heterogeneous', #15
+ 'lossy + source.u (additive) + homogeneous', #16
+ 'lossy + source.u (additive) + heterogeneous', #17
+ 'lossy + source.u (dirichlet) + homogeneous', #18
+ 'lossy + source.u (dirichlet) + heterogeneous' #19
+ ]
+
+ # lists used to set properties
+ p0_tests = [0, 1, 10, 11]
+ s_tests = [2, 3, 4, 5, 12, 13, 14, 15]
+ u_tests = [6, 7, 8, 9, 16, 17, 18, 19]
+ dirichlet_tests = [4, 5, 8, 9, 14, 15, 18, 19]
+
+ # =========================================================================
+ # SIMULATIONS
+ # =========================================================================
+
+ # loop through tests
+ for test_num in [17,]: # np.arange(start=1, stop=2, step=1, dtype=int):
+ # np.arange(1, 21, dtype=int):
+
+ test_name = test_names[test_num]
+
+ # update command line
+ print('Running Test: ', test_name)
+
+ # assign medium properties
+ medium = kWaveMedium(sound_speed=cp1,
+ density=rho1,
+ sound_speed_compression=cp1,
+ sound_speed_shear=cs1)
+
+ # if lossy include loss terms and set flag
+ if test_num > 9:
+ medium.alpha_coeff_compression = alpha_p1
+ medium.alpha_coeff_shear = alpha_s1
+
+ # ----------------
+ # 2D SIMULATION
+ # ----------------
+
+ # create computational grid
+ kgrid = kWaveGrid(Vector([Nx, Ny]), Vector([dx, dy]))
+
+ # heterogeneous medium properties
+ if bool(rem(test_num, 2)):
+ if verbose:
+ print("Set material properties [2d] as hetrogeneous: ", bool(rem(test_num, 2)), "for", test_num)
+ setMaterialProperties(medium, N1=Nx, N2=Ny, N3=int(1), direction=int(1), interface_position=interface_position, cp1=cp1, cs1=cs1, rho1=rho1)
+ else:
+ pass
+
+ # define time array
+ cfl: float = 0.1
+ t_end: float = 3e-6
+ kgrid.dt = cfl * kgrid.dx / cp1
+ kgrid.Nt = int(round(t_end / kgrid.dt))
+ kgrid.t_array = np.arange(0, kgrid.Nt) * kgrid.dt
+
+ offset: int = 1
+
+ # define sensor mask
+ sensor_mask_2D = make_circle(Vector([Nx, Ny]), Vector([Nx // 2 , Ny // 2]), 15)
+
+ # define source properties
+ source_strength: float = 3.0 # [Pa]
+ source_position_x: int = Nx // 2 - 20 - offset
+ source_position_y: int = Ny // 2 - 10 - offset
+ source_freq: float = 2e6 # [Hz]
+ source_signal = source_strength * np.sin(2.0 * np.pi * source_freq * kgrid.t_array)
+
+ # sensor
+ sensor = kSensor()
+ sensor.record = ['u']
+
+ # source
+ source = kSource()
+ if test_num in p0_tests:
+ p0 = np.zeros((Nx, Ny))
+ p0[source_position_x, source_position_y] = source_strength
+ if SMOOTH_P0_SOURCE:
+ p0 = smooth(p0, True)
+ source.p0 = p0
+
+ elif test_num in s_tests:
+ source.s_mask = np.zeros((Nx, Ny), dtype=bool)
+ source.s_mask[source_position_x, source_position_y] = True
+ source.sxx = source_signal
+ source.syy = source_signal
+ if test_num in dirichlet_tests:
+ source.s_mode = 'dirichlet'
+
+ elif test_num in u_tests:
+ source.u_mask = np.zeros((Nx, Ny), dtype=bool)
+ source.u_mask[source_position_x, source_position_y] = True
+ source.ux = source_signal / (cp1 * rho1)
+ source.uy = source_signal / (cp1 * rho1)
+ if test_num in dirichlet_tests:
+ source.u_mode = 'dirichlet'
+
+ else:
+ raise RuntimeError('Unknown source condition.')
+
+ # sensor mask
+ sensor.mask = np.zeros((Nx, Ny), dtype=int)
+ sensor.mask[:, :] = sensor_mask_2D
+
+ # run the simulation
+ simulation_options_2d = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_size=[pml_size, pml_size],
+ pml_x_size=pml_size,
+ pml_y_size=pml_size,
+ pml_x_alpha=pml_alpha,
+ pml_y_alpha=pml_alpha,
+ smooth_p0=SMOOTH_P0_SOURCE, smooth_rho0=SMOOTH_P0_SOURCE, smooth_c0=SMOOTH_P0_SOURCE)
+
+ sensor_data_2D = pstd_elastic_2d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options_2d))
+
+ # calculate velocity amplitude
+ sensor_data_2D['ux'] = np.reshape(sensor_data_2D['ux'], sensor_data_2D['ux'].shape, order='F')
+ sensor_data_2D['uy'] = np.reshape(sensor_data_2D['uy'], sensor_data_2D['uy'].shape, order='F')
+ sensor_2D = np.sqrt(sensor_data_2D['ux']**2 + sensor_data_2D['uy']**2)
+
+
+
+
+ # ----------------
+ # 3D SIMULATION: Z
+ # ----------------
+
+ del kgrid
+ del source
+ del sensor
+
+ # create computational grid
+ kgrid = kWaveGrid(Vector([Nx, Ny, Nz]), Vector([dx, dy, dz]))
+
+ # heterogeneous medium properties
+ if bool(rem(test_num, 2)):
+ if verbose:
+ print("SET MATERIALS [3D Z] AS Hetrogeneous:", bool(rem(test_num, 2)), "for", test_num)
+ setMaterialProperties(medium, N1=Nx, N2=Ny, N3=Nz, direction=1, interface_position=interface_position, cp1=cp1, cs1=cs1, rho1=rho1)
+ else:
+ pass
+
+ # define time array
+ cfl: float = 0.1
+ t_end: float = 3e-6
+ kgrid.dt = cfl * kgrid.dx / cp1
+ kgrid.Nt = int(round(t_end / kgrid.dt))
+ kgrid.t_array = np.arange(0, kgrid.Nt) * kgrid.dt
+
+ # source
+ source = kSource()
+ if test_num in p0_tests:
+ p0 = np.zeros((Nx, Ny, Nz))
+ p0[source_position_x, source_position_y, :] = source_strength
+ if SMOOTH_P0_SOURCE:
+ p0 = smooth(p0, True)
+ source.p0 = p0
+
+ elif test_num in s_tests:
+ source.s_mask = np.zeros((Nx, Ny, Nz))
+ source.s_mask[source_position_x, source_position_y, :] = 1
+ source.sxx = source_signal
+ source.syy = source_signal
+ source.szz = source_signal
+ if test_num in dirichlet_tests:
+ source.s_mode = 'dirichlet'
+
+ elif test_num in u_tests:
+ source.u_mask = np.zeros((Nx, Ny, Nz), dtype=bool)
+ source.u_mask[source_position_x, source_position_y, :] = True
+ source.ux = source_signal / (cp1 * rho1)
+ source.uy = source_signal / (cp1 * rho1)
+ source.uz = source_signal / (cp1 * rho1)
+ if test_num in dirichlet_tests:
+ source.u_mode = 'dirichlet'
+
+ else:
+ raise RuntimeError('Unknown source condition.')
+
+ # sensor
+ sensor = kSensor()
+ sensor.record = ['u']
+ sensor.mask = np.zeros((Nx, Ny, Nz))
+ sensor.mask[:, :, Nz // 2 - 1] = sensor_mask_2D
+
+ # run the simulation
+ simulation_options_3d = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ pml_x_size=pml_size,
+ pml_y_size=pml_size,
+ pml_z_size=pml_size,
+ pml_x_alpha=pml_alpha,
+ pml_y_alpha=pml_alpha,
+ pml_z_alpha=0.0,
+ smooth_p0=SMOOTH_P0_SOURCE, smooth_c0=SMOOTH_P0_SOURCE, smooth_rho0=SMOOTH_P0_SOURCE)
+
+ sensor_data_3D_z = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ source=deepcopy(source),
+ sensor=deepcopy(sensor),
+ medium=deepcopy(medium),
+ simulation_options=deepcopy(simulation_options_3d))
+
+ if verbose:
+ print(np.shape(sensor_data_3D_z['ux']), np.shape(sensor_data_3D_z['uy']), pml_size, Nx, kgrid.Nx, Ny, kgrid.Ny, kgrid.Nt)
+
+ # calculate velocity amplitude
+ sensor_data_3D_z['ux'] = np.reshape(sensor_data_3D_z['ux'], sensor_data_3D_z['ux'].shape, order='F')
+ sensor_data_3D_z['uy'] = np.reshape(sensor_data_3D_z['uy'], sensor_data_3D_z['uy'].shape, order='F')
+ sensor_3D_z = np.sqrt(sensor_data_3D_z['ux']**2 + sensor_data_3D_z['uy']**2)
+
+ # # ----------------
+ # # 3D SIMULATION: Y
+ # # ----------------
+
+ # del kgrid
+ # del source
+ # del sensor
+
+ # # create computational grid
+ # kgrid = kWaveGrid(Vector([Nx, Nz, Ny]), Vector([dx, dz, dy]))
+
+ # # heterogeneous medium properties
+ # if bool(rem(test_num, 2)):
+ # if verbose:
+ # print("SET MATERIALS [3D Y] AS Hetrogeneous:", bool(rem(test_num, 2)), "for", test_num)
+ # setMaterialProperties(medium, Nx, Nz, Ny, direction=1, interface_position=interface_position, cp1=cp1, cs1=cs1, rho1=rho1)
+ # c_max = np.max(np.asarray([np.max(medium.sound_speed_compression), np.max(medium.sound_speed_shear)]))
+ # else:
+ # c_max = np.max(medium.sound_speed_compression)
+
+ # # define time array
+ # # cfl = 0.1
+ # # t_end = 3e-6
+ # # kgrid.makeTime(c_max, cfl, t_end)
+ # cfl: float = 0.1
+ # t_end: float = 3e-6
+ # kgrid.dt = cfl * kgrid.dx / cp1
+ # kgrid.Nt = int(round(t_end / kgrid.dt))
+ # kgrid.t_array = np.arange(0, kgrid.Nt) * kgrid.dt
+
+ # # source
+ # source = kSource()
+ # if test_num in p0_tests:
+ # p0 = np.zeros((Nx, Nz, Ny))
+ # p0[source_position_x, :, source_position_y] = source_strength
+ # if SMOOTH_P0_SOURCE:
+ # p0 = smooth(p0, True)
+ # source.p0 = p0
+
+ # elif test_num in s_tests:
+ # source.s_mask = np.zeros((Nx, Nz, Ny))
+ # source.s_mask[source_position_x, :, source_position_y] = 1
+ # source.sxx = source_signal
+ # source.syy = source_signal
+ # source.szz = source_signal
+ # if test_num in dirichlet_tests:
+ # source.s_mode = 'dirichlet'
+
+ # elif test_num in u_tests:
+ # source.u_mask = np.zeros((Nx, Nz, Ny))
+ # source.u_mask[source_position_x, :, source_position_y] = 1
+ # source.ux = source_signal / (cp1 * rho1)
+ # source.uy = source_signal / (cp1 * rho1)
+ # source.uz = source_signal / (cp1 * rho1)
+ # if test_num in dirichlet_tests:
+ # source.u_mode = 'dirichlet'
+
+ # else:
+ # raise RuntimeError('Unknown source condition.')
+
+ # # sensor
+ # sensor = kSensor()
+ # sensor.record = ['u']
+ # sensor.mask = np.zeros((Nx, Nz, Ny)) #, order='F')
+ # sensor.mask[:, Nz // 2 - 1, :] = sensor_mask_2D
+
+ # # run the simulation
+ # simulation_options_3d = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ # pml_size=pml_size,
+ # pml_x_alpha=pml_alpha,
+ # pml_y_alpha=0.0,
+ # pml_z_alpha=pml_alpha,
+ # smooth_p0=SMOOTH_P0_SOURCE, smooth_c0=SMOOTH_P0_SOURCE, smooth_rho0=SMOOTH_P0_SOURCE)
+
+ # sensor_data_3D_y = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ # source=deepcopy(source),
+ # sensor=deepcopy(sensor),
+ # medium=deepcopy(medium),
+ # simulation_options=deepcopy(simulation_options_3d))
+
+ # # calculate velocity amplitude
+ # sensor_data_3D_y['ux'] = np.reshape(sensor_data_3D_y['ux'], sensor_data_3D_y['ux'].shape, order='F')
+ # sensor_data_3D_y['uz'] = np.reshape(sensor_data_3D_y['uz'], sensor_data_3D_y['uz'].shape, order='F')
+ # sensor_data_3D_y = np.sqrt(sensor_data_3D_y['ux']**2 + sensor_data_3D_y['uz']**2)
+
+ # # ----------------
+ # # 3D SIMULATION: X
+ # # ----------------
+
+ # del kgrid
+ # del source
+ # del sensor
+
+ # # create computational grid
+ # kgrid = kWaveGrid(Vector([Nz, Nx, Ny]), Vector([dz, dx, dy]))
+
+ # # heterogeneous medium properties
+ # if bool(rem(test_num, 2)):
+ # if verbose:
+ # print("SET MATERIALS [3D X] AS Hetrogeneous:", bool(rem(test_num, 2)), "for", test_num)
+ # setMaterialProperties(medium, Nz, Nx, Ny, direction=2, interface_position=interface_position, cp1=cp1, cs1=cs1, rho1=rho1)
+ # c_max = np.max(np.asarray([np.max(medium.sound_speed_compression), np.max(medium.sound_speed_shear)]))
+ # else:
+ # c_max = np.max(medium.sound_speed_compression)
+
+ # # define time array
+ # # cfl = 0.1
+ # # t_end = 3e-6
+ # # kgrid.makeTime(c_max, cfl, t_end)
+ # cfl: float = 0.1
+ # t_end: float = 3e-6
+ # kgrid.dt = cfl * kgrid.dx / cp1
+ # kgrid.Nt = int(round(t_end / kgrid.dt))
+ # kgrid.t_array = np.arange(0, kgrid.Nt) * kgrid.dt
+
+
+ # # source
+ # source = kSource()
+ # if test_num in p0_tests:
+ # p0 = np.zeros((Nz, Nx, Ny))
+ # p0[:, source_position_x, source_position_y] = source_strength
+ # if SMOOTH_P0_SOURCE:
+ # p0 = smooth(p0, True)
+ # source.p0 = p0
+
+ # elif test_num in s_tests:
+ # source.s_mask = np.zeros((Nz, Nx, Ny))
+ # source.s_mask[:, source_position_x, source_position_y] = 1
+ # source.sxx = source_signal
+ # source.syy = source_signal
+ # source.szz = source_signal
+ # if test_num in dirichlet_tests:
+ # source.s_mode = 'dirichlet'
+
+ # elif test_num in u_tests:
+ # source.u_mask = np.zeros((Nz, Nx, Ny))
+ # source.u_mask[:, source_position_x, source_position_y] = 1
+ # source.ux = source_signal / (cp1 * rho1)
+ # source.uy = source_signal / (cp1 * rho1)
+ # source.uz = source_signal / (cp1 * rho1)
+ # if test_num in dirichlet_tests:
+ # source.u_mode = 'dirichlet'
+
+ # else:
+ # raise RuntimeError('Unknown source condition.')
+
+ # # sensor
+ # sensor = kSensor()
+ # sensor.record = ['u']
+ # sensor.mask = np.zeros((Nz, Nx, Ny)) #, order='F')
+ # sensor.mask[Nz // 2 - 1, :, :] = sensor_mask_2D
+
+ # # run the simulation
+ # simulation_options_3d = SimulationOptions(simulation_type=SimulationType.ELASTIC,
+ # pml_size=pml_size,
+ # pml_x_alpha=0.0,
+ # pml_y_alpha=pml_alpha,
+ # pml_z_alpha=pml_alpha,
+ # smooth_p0=SMOOTH_P0_SOURCE, smooth_c0=SMOOTH_P0_SOURCE, smooth_rho0=SMOOTH_P0_SOURCE)
+
+ # sensor_data_3D_x = pstd_elastic_3d(kgrid=deepcopy(kgrid),
+ # source=deepcopy(source),
+ # sensor=deepcopy(sensor),
+ # medium=deepcopy(medium),
+ # simulation_options=deepcopy(simulation_options_3d))
+
+ # # calculate velocity amplitude
+ # sensor_data_3D_x['uy'] = np.reshape(sensor_data_3D_x['uy'], sensor_data_3D_x['uy'].shape, order='F')
+ # sensor_data_3D_x['uz'] = np.reshape(sensor_data_3D_x['uz'], sensor_data_3D_x['uz'].shape, order='F')
+ # sensor_data_3D_x = np.sqrt(sensor_data_3D_x['uy']**2 + sensor_data_3D_x['uz']**2)
+
+ # -------------
+ # COMPARISON
+ # -------------
+
+ if (test_num == 0):
+ matlab_test = loadmat("C:/Users/dsinden/dev/octave/k-Wave/sensor_data_2D_num17.mat")
+ matlab_2d = matlab_test['sensor_2d']
+
+ if (test_num == 17):
+ matlab_dict = loadmat("C:/Users/dsinden/dev/octave/k-Wave/sensor_data_2D_num18.mat")
+ matlab_2d = matlab_dict['sensor_data_2D']
+ # print(matlab_data)
+ # print(matlab_data[0][0][0])
+ # matlab_2d_uy = np.reshape(matlab_data[0][0][0], matlab_data[0][0][0].shape, order='F')
+ # matlab_2d_ux = np.reshape(matlab_data[0][0][1], matlab_data[0][0][1].shape, order='F')
+ # matlab_2d = np.sqrt(matlab_2d_uy**2 + matlab_2d_ux**2)
+
+ matlab_dict = loadmat("C:/Users/dsinden/dev/octave/k-Wave/sensor_data_3Dz_num18")
+ matlab_3d = matlab_dict['sensor_data_3D_z']
+
+ if (test_num == 16):
+ matlab_dict = loadmat("C:/Users/dsinden/dev/octave/sensor_data_2D_num17.mat")
+ matlab_2d = matlab_dict['sensor_data_2D']
+ matlab_dict = loadmat("C:/Users/dsinden/dev/octave/sensor_data_3Dz_num17.mat")
+ matlab_3d = matlab_dict['sensor_data_3D_z']
+
+ if verbose:
+ if ((test_num == 0) or (test_num == 17) or (test_num == 16)):
+ print(np.unravel_index(np.argmax(np.abs(matlab_2d)), matlab_2d.shape, order='F'),
+ np.unravel_index(np.argmax(np.abs(matlab_3d)), matlab_3d.shape, order='F'),
+ np.unravel_index(np.argmax(np.abs(sensor_2D)), sensor_2D.shape, order='F'),
+ np.unravel_index(np.argmax(np.abs(sensor_3D_z)), sensor_3D_z.shape, order='F'),
+ # np.unravel_index(np.argmax(np.abs(sensor_data_3D_y)), sensor_data_3D_y.shape, order='F'),
+ # np.unravel_index(np.argmax(np.abs(sensor_data_3D_x)), sensor_data_3D_x.shape, order='F'),
+ )
+ else:
+ print(np.unravel_index(np.argmax(np.abs(sensor_2D)), sensor_2D.shape, order='F'),
+ np.unravel_index(np.argmax(np.abs(sensor_3D_z)), sensor_3D_z.shape, order='F'),
+ # np.unravel_index(np.argmax(np.abs(sensor_data_3D_y)), sensor_data_3D_y.shape, order='F'),
+ # np.unravel_index(np.argmax(np.abs(sensor_data_3D_x)), sensor_data_3D_x.shape, order='F'),
+ )
+
+ # sensor_data_3D_z = np.zeros_like(sensor_2D)
+ sensor_data_3D_y = np.zeros_like(sensor_2D)
+ sensor_data_3D_x = np.zeros_like(sensor_2D)
+
+ max2d = np.max(np.abs(sensor_2D))
+ max3d_z = np.max(np.abs(sensor_3D_z))
+ max3d_y = np.max(np.abs(sensor_data_3D_y))
+ max3d_x = np.max(np.abs(sensor_data_3D_x))
+
+ diff_2D_3D_z = np.max(np.abs(sensor_2D - sensor_3D_z)) / max2d
+ if diff_2D_3D_z > COMPARISON_THRESH:
+ test_pass = False
+ msg = f"Not equal: diff_2D_3D_z: {diff_2D_3D_z} and 2d: {max2d}, 3d: {max3d_z}"
+ print(msg)
+ all_tests = all_tests and test_pass
+
+ diff_2D_3D_y = np.max(np.abs(sensor_2D - sensor_data_3D_y)) / max2d
+ if diff_2D_3D_y > COMPARISON_THRESH:
+ test_pass = False
+ msg = f"Not equal: diff_2D_3D_y: {diff_2D_3D_y} and 2d: {max2d}, 3d: {max3d_y}"
+ print(msg)
+ all_tests = all_tests and test_pass
+
+ diff_2D_3D_x = np.max(np.abs(sensor_2D - sensor_data_3D_x)) / max2d
+ if diff_2D_3D_x > COMPARISON_THRESH:
+ test_pass = False
+ msg = f"Not equal: diff_2D_3D_x: {diff_2D_3D_x} and 2d: {max2d}, 3d: {max3d_x}"
+ print(msg)
+ all_tests = all_tests and test_pass
+
+ # if (test_num == 0):
+ # fig3, ((ax3a, ax3b), (ax3c, ax3d)) = plt.subplots(2, 2)
+ # fig3.suptitle(f"{test_name}: Z")
+ # ax3a.imshow(sensor_2D)
+ # # ax3b.imshow(sensor_3D_z)
+ # # ax3c.imshow(np.abs(sensor_2D - sensor_3D_z))
+ # ax3d.imshow(np.abs(matlab_2d))
+ # else:
+ # fig3, (ax3a, ax3b, ax3c) = plt.subplots(3, 1)
+ # fig3.suptitle(f"{test_name}: Z")
+ # ax3a.imshow(sensor_2D)
+ # # ax3b.imshow(sensor_3D_z)
+ # # ax3c.imshow(np.abs(sensor_2D - sensor_3D_z))
+
+ # fig2, ((ax2a, ax2b, ax2c) ) = plt.subplots(3, 1)
+ # fig2.suptitle(f"{test_name}: Y")
+ # ax2a.imshow(sensor_2D)
+ # # ax2b.imshow(sensor_data_3D_y)
+ # # ax2c.imshow(np.abs(sensor_2D - sensor_data_3D_y))
+
+ # fig1, ((ax1a, ax1b, ax1c) ) = plt.subplots(3, 1)
+ # fig1.suptitle(f"{test_name}: X")
+ # ax1a.imshow(sensor_2D)
+ # ax1b.imshow(sensor_data_3D_x)
+ # ax1c.imshow(np.abs(sensor_2D - sensor_data_3D_x))
+
+
+ fig0, ax0a = plt.subplots(1, 1)
+ N2 = np.shape(sensor_2D)[0]
+ # print(N2)
+ # N3x = np.shape(sensor_data_3D_x)[0]
+ # N3y = np.shape(sensor_data_3D_y)[0]
+ N3z = np.shape(sensor_3D_z)[0]
+ # print(N3z)
+ ax0a.plot(np.squeeze(kgrid.t_array), sensor_2D[N2 // 2 - 1, :], label='2D')
+ # ax0a.plot(np.squeeze(kgrid.t_array), sensor_data_3D_x[Nx // 2 - 1, :], label='3D x')
+ # ax0a.plot(np.squeeze(kgrid.t_array), sensor_data_3D_y[Ny // 2 - 1, :], label='3D y')
+ ax0a.plot(np.squeeze(kgrid.t_array), sensor_3D_z[N3z // 2 - 1, :],
+ color='tab:orange', linestyle='--', marker='o', markerfacecolor='none', markeredgecolor='tab:orange', label='3D z')
+ if ((test_num == 0) or (test_num == 16) or (test_num == 17)):
+ ax0a.plot(np.squeeze(kgrid.t_array), matlab_2d[np.shape(matlab_2d)[0] // 2 - 1, :], 'k-', label='matlab 2d')
+ ax0a.plot(np.squeeze(kgrid.t_array), matlab_3d[np.shape(matlab_3d)[0] // 2 - 1, :], 'k--*', label='matlab 3d')
+ ax0a.legend()
+ ax0a.set_ylim(-1e-6, 1e-6)
+ fig0.suptitle(f"{test_name}")
+ # ax0b.plot(np.squeeze(kgrid.t_array), sensor_2D[:, Ny // 2 - 1], label='2D')
+ # ax0b.plot(np.squeeze(kgrid.t_array), sensor_data_3D_x[:, Ny // 2 - 1], label='3D x')
+ # ax0b.plot(np.squeeze(kgrid.t_array), sensor_data_3D_y[:, Ny // 2 - 1], label='3D y')
+ # ax0b.plot(np.squeeze(kgrid.t_array), sensor_3D_z[:, Ny // 2 - 1], label='3D z')
+ # ax0b.legend()
+
+ fig1, ax1a = plt.subplots(1, 1)
+ N2 = np.shape(sensor_2D)[0]
+ ax1a.plot(sensor_data_2D['ux'][N2 // 2 - 1, :], label='ux 2D')
+ ax1a.plot(sensor_data_2D['uy'][N2 // 2 - 1, :], label='uy 2D')
+ ax1a.plot(sensor_data_3D_z['ux'][N3z // 2 - 1, :], label='ux 3D')
+ ax1a.plot(sensor_data_3D_z['uy'][N3z // 2 - 1, :], label='uy 3D')
+ ax1a.legend()
+ ax1a.set_ylim(-1e-6, 1e-6)
+ fig1.suptitle(f"{test_name}")
+
+
+ # clear structures
+ del kgrid
+ del source
+ del medium
+ del sensor
+
+ plt.show()
+
+ assert all_tests, msg
+
+ # diff_2D_3D_x = np.max(np.abs(sensor_2D - sensor_data_3D_x)) / ref_max
+ # if diff_2D_3D_x > COMPARISON_THRESH:
+ # test_pass = False
+ # assert test_pass, "Not equal: dff_2D_3D_x"
+
+ # diff_2D_3D_y = np.max(np.abs(sensor_2D - sensor_data_3D_y)) / ref_max
+ # if diff_2D_3D_y > COMPARISON_THRESH:
+ # test_pass = False
+ # assert test_pass, "Not equal: diff_2D_3D_y"
+
+
+
+
+