fullwave25 1.2.6.dev1__tar.gz → 1.2.6.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/.bumpversion.toml +1 -1
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/PKG-INFO +1 -1
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/__init__.py +1 -1
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/solver.py +104 -1
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/__init__.py +2 -1
- fullwave25-1.2.6.dev2/fullwave/utils/signal_filter.py +161 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/pyproject.toml +1 -1
- fullwave25-1.2.6.dev2/signal_filter_example.png +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/uv.lock +1 -1
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/.gitignore +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/.pre-commit-config.yaml +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/.python-version +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/20260120_0844.log +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/CITATION.cff +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/CLAUDE.md +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/CONTRIBUTING.md +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/LICENSE +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/Makefile +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/README.md +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/beamformer.m +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/debug_solver_bin/fullwave2_3d_exponential_attenuation_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/experiments/__init__.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/experiments/exp_scatterer_diameter.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/beamformer/__init__.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/beamformer/beamformer.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/constants/__init__.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/constants/material_properties.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/grid.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/__init__.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/domain.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/medium_builder.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/__init__.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/data/.keep +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_abdominal_wall.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_background.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_scatterer.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_simple.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_water_gel.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/sensor.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/__init__.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/binary_manager.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda129 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda130 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda118 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda129 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda130 +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/database/relaxation_params_database_num_relax=2_20260113_0957.mat +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/cuda_utils.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/input_file_writer.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/launcher.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/pml_builder.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/utils.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/source.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/transducer.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/check_functions.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/coordinates.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/memory_tempfile.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/numerical.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/plot_utils.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/pulse.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/relaxation_parameters.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/scatterer.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/signal_process.py +0 -0
- {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/ruff.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fullwave25
|
|
3
|
-
Version: 1.2.6.
|
|
3
|
+
Version: 1.2.6.dev2
|
|
4
4
|
Summary: Fullwave 2.5: Ultrasound wave propagation simulation with heterogeneous power law attenuation modelling capabilities
|
|
5
5
|
Author: Masashi Sode, Gianmarco Pinton
|
|
6
6
|
Maintainer: Masashi Sode
|
|
@@ -60,7 +60,7 @@ try:
|
|
|
60
60
|
__version__ = version("fullwave")
|
|
61
61
|
except PackageNotFoundError:
|
|
62
62
|
# Update via bump-my-version, not manually
|
|
63
|
-
__version__ = "1.2.6-
|
|
63
|
+
__version__ = "1.2.6-dev2"
|
|
64
64
|
|
|
65
65
|
VERSION = __version__ # for convenience
|
|
66
66
|
logger.info("Fullwave version: %s", __version__)
|
|
@@ -15,6 +15,7 @@ from fullwave.utils import (
|
|
|
15
15
|
MemoryTempfile,
|
|
16
16
|
check_functions,
|
|
17
17
|
)
|
|
18
|
+
from fullwave.utils.signal_filter import apply_filter
|
|
18
19
|
|
|
19
20
|
from .binary_manager import ensure_binary
|
|
20
21
|
from .cuda_utils import get_cuda_architecture, retrieve_cuda_version
|
|
@@ -638,6 +639,80 @@ class Solver:
|
|
|
638
639
|
error_msg = f"{path_fullwave_simulation_bin} does not exist"
|
|
639
640
|
assert path_fullwave_simulation_bin.exists(), error_msg
|
|
640
641
|
|
|
642
|
+
@staticmethod
|
|
643
|
+
def _validate_filter_params(
|
|
644
|
+
highpass_cutoff_mhz: float | None,
|
|
645
|
+
bandpass_cutoff_mhz: tuple[float, float] | None,
|
|
646
|
+
*,
|
|
647
|
+
load_results: bool,
|
|
648
|
+
) -> None:
|
|
649
|
+
"""Validate high-pass / band-pass filter arguments passed to run().
|
|
650
|
+
|
|
651
|
+
Raises
|
|
652
|
+
------
|
|
653
|
+
ValueError
|
|
654
|
+
If both filter options are set simultaneously, or if a filter is
|
|
655
|
+
requested without ``load_results=True``.
|
|
656
|
+
|
|
657
|
+
"""
|
|
658
|
+
if highpass_cutoff_mhz is not None and bandpass_cutoff_mhz is not None:
|
|
659
|
+
error_msg = (
|
|
660
|
+
"highpass_cutoff_mhz and bandpass_cutoff_mhz cannot both be specified. "
|
|
661
|
+
"Use highpass_cutoff_mhz for a simple high-pass filter or "
|
|
662
|
+
"bandpass_cutoff_mhz for a band-pass filter."
|
|
663
|
+
)
|
|
664
|
+
logger.error(error_msg)
|
|
665
|
+
raise ValueError(error_msg)
|
|
666
|
+
if (highpass_cutoff_mhz is not None or bandpass_cutoff_mhz is not None) and (
|
|
667
|
+
not load_results
|
|
668
|
+
):
|
|
669
|
+
error_msg = (
|
|
670
|
+
"Filtering requires load_results=True. "
|
|
671
|
+
"Set load_results=True or disable the filter options."
|
|
672
|
+
)
|
|
673
|
+
logger.error(error_msg)
|
|
674
|
+
raise ValueError(error_msg)
|
|
675
|
+
|
|
676
|
+
@staticmethod
|
|
677
|
+
def _apply_output_filter(
|
|
678
|
+
result: NDArray[np.float64],
|
|
679
|
+
dt: float,
|
|
680
|
+
highpass_cutoff_mhz: float | None,
|
|
681
|
+
bandpass_cutoff_mhz: tuple[float, float] | None,
|
|
682
|
+
) -> NDArray[np.float64]:
|
|
683
|
+
"""Apply the optional frequency filter to the reshaped sensor output.
|
|
684
|
+
|
|
685
|
+
Parameters
|
|
686
|
+
----------
|
|
687
|
+
result : NDArray[np.float64]
|
|
688
|
+
Sensor data shaped ``[n_sensors, n_t]``.
|
|
689
|
+
dt : float
|
|
690
|
+
Grid time step in seconds.
|
|
691
|
+
highpass_cutoff_mhz : float | None
|
|
692
|
+
High-pass edge in MHz, or ``None``.
|
|
693
|
+
bandpass_cutoff_mhz : tuple[float, float] | None
|
|
694
|
+
``(f_low_mhz, f_high_mhz)`` band-pass edges, or ``None``.
|
|
695
|
+
|
|
696
|
+
Returns
|
|
697
|
+
-------
|
|
698
|
+
NDArray[np.float64]
|
|
699
|
+
Filtered (or unchanged) sensor data.
|
|
700
|
+
|
|
701
|
+
"""
|
|
702
|
+
if highpass_cutoff_mhz is not None:
|
|
703
|
+
logger.info("Applying high-pass filter at %.4g MHz...", highpass_cutoff_mhz)
|
|
704
|
+
return apply_filter(result, dt, f_low_hz=highpass_cutoff_mhz * 1e6)
|
|
705
|
+
if bandpass_cutoff_mhz is not None:
|
|
706
|
+
f_low_hz = bandpass_cutoff_mhz[0] * 1e6
|
|
707
|
+
f_high_hz = bandpass_cutoff_mhz[1] * 1e6
|
|
708
|
+
logger.info(
|
|
709
|
+
"Applying band-pass filter %.4g-%.4g MHz...",
|
|
710
|
+
bandpass_cutoff_mhz[0],
|
|
711
|
+
bandpass_cutoff_mhz[1],
|
|
712
|
+
)
|
|
713
|
+
return apply_filter(result, dt, f_low_hz=f_low_hz, f_high_hz=f_high_hz)
|
|
714
|
+
return result
|
|
715
|
+
|
|
641
716
|
@staticmethod
|
|
642
717
|
def _reshape_sensor_data(
|
|
643
718
|
raw_sensor_output: NDArray[np.float64],
|
|
@@ -681,6 +756,8 @@ class Solver:
|
|
|
681
756
|
load_results: bool = True,
|
|
682
757
|
generate_input_only: bool = False,
|
|
683
758
|
release_after_write: bool = False,
|
|
759
|
+
highpass_cutoff_mhz: float | None = None,
|
|
760
|
+
bandpass_cutoff_mhz: tuple[float, float] | None = None,
|
|
684
761
|
) -> NDArray[np.float64] | Path:
|
|
685
762
|
r"""Run the fullwave simulation and return the result as a NumPy array.
|
|
686
763
|
|
|
@@ -740,6 +817,18 @@ class Solver:
|
|
|
740
817
|
If True, the memory used by the input files will be released after writing them to disk.
|
|
741
818
|
This is useful when run_on_memory is True to free up memory space for the simulation
|
|
742
819
|
or when the input files are large. Default is False.
|
|
820
|
+
highpass_cutoff_mhz : float | None
|
|
821
|
+
Apply a high-pass filter to the sensor recordings after the simulation.
|
|
822
|
+
Removes low-frequency PML drift by attenuating frequencies below this value (in MHz).
|
|
823
|
+
Uses a cosine (Hann) taper to avoid Gibbs ringing.
|
|
824
|
+
Cannot be combined with ``bandpass_cutoff_mhz``.
|
|
825
|
+
Requires ``load_results=True``. Default is ``None`` (no filtering).
|
|
826
|
+
bandpass_cutoff_mhz : tuple[float, float] | None
|
|
827
|
+
Apply a band-pass filter ``(f_low_mhz, f_high_mhz)`` to the sensor recordings
|
|
828
|
+
after the simulation. Retains only frequencies inside the specified band.
|
|
829
|
+
Uses cosine (Hann) tapers on both edges.
|
|
830
|
+
Cannot be combined with ``highpass_cutoff_mhz``.
|
|
831
|
+
Requires ``load_results=True``. Default is ``None`` (no filtering).
|
|
743
832
|
|
|
744
833
|
Returns
|
|
745
834
|
-------
|
|
@@ -757,6 +846,8 @@ class Solver:
|
|
|
757
846
|
Static map simulations require input files to be stored on a disk.
|
|
758
847
|
run_on_memory, on the other hand, removes the input files
|
|
759
848
|
after the simulation is complete.
|
|
849
|
+
Also raised if both ``highpass_cutoff_mhz`` and ``bandpass_cutoff_mhz`` are given,
|
|
850
|
+
or if either filter option is set but ``load_results=False``.
|
|
760
851
|
|
|
761
852
|
"""
|
|
762
853
|
# self._save_data_for_beamforming()
|
|
@@ -778,6 +869,12 @@ class Solver:
|
|
|
778
869
|
logger.error(error_msg)
|
|
779
870
|
raise ValueError(error_msg)
|
|
780
871
|
|
|
872
|
+
self._validate_filter_params(
|
|
873
|
+
highpass_cutoff_mhz,
|
|
874
|
+
bandpass_cutoff_mhz,
|
|
875
|
+
load_results=load_results,
|
|
876
|
+
)
|
|
877
|
+
|
|
781
878
|
start_time = time.time()
|
|
782
879
|
extended_medium = self.pml_builder.run(use_pml=self.use_pml)
|
|
783
880
|
end_pml_builder_time = time.time()
|
|
@@ -867,7 +964,13 @@ class Solver:
|
|
|
867
964
|
f"{end_loading_time - start_loading_time:.2e} seconds."
|
|
868
965
|
)
|
|
869
966
|
logger.info(message)
|
|
870
|
-
|
|
967
|
+
|
|
968
|
+
return self._apply_output_filter(
|
|
969
|
+
result,
|
|
970
|
+
self.grid.dt,
|
|
971
|
+
highpass_cutoff_mhz,
|
|
972
|
+
bandpass_cutoff_mhz,
|
|
973
|
+
)
|
|
871
974
|
# if load_results is False, return the raw result
|
|
872
975
|
# which is a list of file names
|
|
873
976
|
return sim_result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""misc utils for fullwave package."""
|
|
2
2
|
|
|
3
|
-
from . import pulse, relaxation_parameters, signal_process
|
|
3
|
+
from . import pulse, relaxation_parameters, signal_filter, signal_process
|
|
4
4
|
from .memory_tempfile import MemoryTempfile
|
|
5
5
|
from .scatterer import (
|
|
6
6
|
generate_resolution_based_scatterer,
|
|
@@ -14,5 +14,6 @@ __all__ = [
|
|
|
14
14
|
"generate_scatterer",
|
|
15
15
|
"pulse",
|
|
16
16
|
"relaxation_parameters",
|
|
17
|
+
"signal_filter",
|
|
17
18
|
"signal_process",
|
|
18
19
|
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""FFT-based frequency-domain filtering for sensor data.
|
|
2
|
+
|
|
3
|
+
GPU backend: CuPy when available; falls back silently to NumPy.
|
|
4
|
+
No new hard dependencies — CuPy is already listed under the ``examples`` optional extra.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("__main__." + __name__)
|
|
18
|
+
|
|
19
|
+
# Module-level cache to avoid repeated import overhead
|
|
20
|
+
_CUPY_AVAILABLE: bool | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _check_cupy() -> bool:
|
|
24
|
+
"""Return True if CuPy is importable; result is cached after the first call."""
|
|
25
|
+
global _CUPY_AVAILABLE # noqa: PLW0603
|
|
26
|
+
if _CUPY_AVAILABLE is None:
|
|
27
|
+
try:
|
|
28
|
+
import cupy # noqa: F401
|
|
29
|
+
|
|
30
|
+
_CUPY_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
_CUPY_AVAILABLE = False
|
|
33
|
+
return _CUPY_AVAILABLE
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _build_frequency_mask(
|
|
37
|
+
n_fft: int,
|
|
38
|
+
dt: float,
|
|
39
|
+
f_low_hz: float | None = None,
|
|
40
|
+
f_high_hz: float | None = None,
|
|
41
|
+
taper_ratio: float = 0.1,
|
|
42
|
+
) -> NDArray[np.float64]:
|
|
43
|
+
"""Build a frequency-domain gain mask with cosine (Hann) tapers.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
n_fft : int
|
|
48
|
+
FFT length (number of time samples before zero-padding, i.e. ``n_t``).
|
|
49
|
+
dt : float
|
|
50
|
+
Simulation time step in seconds.
|
|
51
|
+
f_low_hz : float | None
|
|
52
|
+
High-pass cut-off frequency in Hz. Frequencies below this value are
|
|
53
|
+
attenuated. The mask transitions smoothly from 0 to 1 in a window of
|
|
54
|
+
width ``f_low_hz * taper_ratio`` centred at ``f_low_hz``.
|
|
55
|
+
f_high_hz : float | None
|
|
56
|
+
Low-pass cut-off frequency in Hz. Frequencies above this value are
|
|
57
|
+
attenuated. The mask transitions smoothly from 1 to 0 in a window of
|
|
58
|
+
width ``f_high_hz * taper_ratio`` centred at ``f_high_hz``.
|
|
59
|
+
taper_ratio : float
|
|
60
|
+
Fractional width of each cosine taper relative to its centre frequency.
|
|
61
|
+
Default is 0.1 (10 %).
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
NDArray[np.float64]
|
|
66
|
+
Frequency-domain gain mask of shape ``[n_fft // 2 + 1]``.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
freqs = np.fft.rfftfreq(n_fft, d=dt)
|
|
70
|
+
mask = np.ones(len(freqs), dtype=np.float64)
|
|
71
|
+
|
|
72
|
+
if f_low_hz is not None:
|
|
73
|
+
half_width = f_low_hz * taper_ratio / 2.0
|
|
74
|
+
f_start = f_low_hz - half_width
|
|
75
|
+
f_end = f_low_hz + half_width
|
|
76
|
+
width = f_end - f_start # == f_low_hz * taper_ratio
|
|
77
|
+
|
|
78
|
+
in_taper = (freqs >= f_start) & (freqs <= f_end)
|
|
79
|
+
below_taper = freqs < f_start
|
|
80
|
+
|
|
81
|
+
mask[below_taper] = 0.0
|
|
82
|
+
mask[in_taper] = 0.5 * (1.0 - np.cos(np.pi * (freqs[in_taper] - f_start) / width))
|
|
83
|
+
|
|
84
|
+
if f_high_hz is not None:
|
|
85
|
+
half_width = f_high_hz * taper_ratio / 2.0
|
|
86
|
+
f_start = f_high_hz - half_width
|
|
87
|
+
f_end = f_high_hz + half_width
|
|
88
|
+
width = f_end - f_start # == f_high_hz * taper_ratio
|
|
89
|
+
|
|
90
|
+
in_taper = (freqs >= f_start) & (freqs <= f_end)
|
|
91
|
+
above_taper = freqs > f_end
|
|
92
|
+
|
|
93
|
+
lp_taper = np.ones(len(freqs), dtype=np.float64)
|
|
94
|
+
lp_taper[in_taper] = 0.5 * (1.0 + np.cos(np.pi * (freqs[in_taper] - f_start) / width))
|
|
95
|
+
lp_taper[above_taper] = 0.0
|
|
96
|
+
mask *= lp_taper
|
|
97
|
+
|
|
98
|
+
return mask
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def apply_filter(
|
|
102
|
+
data: NDArray[np.float64],
|
|
103
|
+
dt: float,
|
|
104
|
+
f_low_hz: float | None = None,
|
|
105
|
+
f_high_hz: float | None = None,
|
|
106
|
+
taper_ratio: float = 0.1,
|
|
107
|
+
*,
|
|
108
|
+
use_gpu: bool = True,
|
|
109
|
+
) -> NDArray[np.float64]:
|
|
110
|
+
"""Apply a frequency-domain filter to sensor data.
|
|
111
|
+
|
|
112
|
+
The filter is built as a cosine-tapered gain mask (see :func:`_build_frequency_mask`).
|
|
113
|
+
When CuPy is available and ``use_gpu=True``, the FFT operations run on the GPU
|
|
114
|
+
for maximum throughput; otherwise NumPy is used transparently.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
data : NDArray[np.float64]
|
|
119
|
+
Sensor time traces, shape ``[n_sensors, n_t]``.
|
|
120
|
+
dt : float
|
|
121
|
+
Simulation time step in seconds.
|
|
122
|
+
f_low_hz : float | None
|
|
123
|
+
High-pass edge frequency in Hz. Pass ``None`` to skip high-passing.
|
|
124
|
+
f_high_hz : float | None
|
|
125
|
+
Low-pass edge frequency in Hz. Pass ``None`` to skip low-passing.
|
|
126
|
+
taper_ratio : float
|
|
127
|
+
Fractional taper width relative to each cut-off frequency. Default 0.1.
|
|
128
|
+
use_gpu : bool
|
|
129
|
+
If ``True`` (default), attempt to use CuPy for GPU-accelerated FFTs.
|
|
130
|
+
Falls back to NumPy silently if CuPy is unavailable.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
NDArray[np.float64]
|
|
135
|
+
Filtered data, same shape as ``data``.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
n_t = data.shape[1]
|
|
139
|
+
mask = _build_frequency_mask(
|
|
140
|
+
n_t,
|
|
141
|
+
dt,
|
|
142
|
+
f_low_hz=f_low_hz,
|
|
143
|
+
f_high_hz=f_high_hz,
|
|
144
|
+
taper_ratio=taper_ratio,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if use_gpu and _check_cupy():
|
|
148
|
+
import cupy as cp
|
|
149
|
+
|
|
150
|
+
logger.debug("apply_filter: using CuPy GPU backend")
|
|
151
|
+
data_gpu = cp.asarray(data, dtype=cp.float64)
|
|
152
|
+
mask_gpu = cp.asarray(mask, dtype=cp.float64)
|
|
153
|
+
spec = cp.fft.rfft(data_gpu, axis=1)
|
|
154
|
+
spec *= mask_gpu[cp.newaxis, :]
|
|
155
|
+
filtered = cp.fft.irfft(spec, n=n_t, axis=1)
|
|
156
|
+
return cp.asnumpy(filtered)
|
|
157
|
+
|
|
158
|
+
logger.debug("apply_filter: using NumPy CPU backend")
|
|
159
|
+
spec = np.fft.rfft(data, axis=1)
|
|
160
|
+
spec *= mask[np.newaxis, :]
|
|
161
|
+
return np.fft.irfft(spec, n=n_t, axis=1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fullwave25"
|
|
3
|
-
version = "1.2.6-
|
|
3
|
+
version = "1.2.6-dev2" # Update via bump-my-version, not manually
|
|
4
4
|
description = "Fullwave 2.5: Ultrasound wave propagation simulation with heterogeneous power law attenuation modelling capabilities"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_background.py
RENAMED
|
File without changes
|
{fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_scatterer.py
RENAMED
|
File without changes
|
{fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_simple.py
RENAMED
|
File without changes
|
{fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_water_gel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|