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" + + + + +