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.
Files changed (70) hide show
  1. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/.bumpversion.toml +1 -1
  2. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/PKG-INFO +1 -1
  3. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/__init__.py +1 -1
  4. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/solver.py +104 -1
  5. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/__init__.py +2 -1
  6. fullwave25-1.2.6.dev2/fullwave/utils/signal_filter.py +161 -0
  7. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/pyproject.toml +1 -1
  8. fullwave25-1.2.6.dev2/signal_filter_example.png +0 -0
  9. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/uv.lock +1 -1
  10. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/.gitignore +0 -0
  11. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/.pre-commit-config.yaml +0 -0
  12. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/.python-version +0 -0
  13. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/20260120_0844.log +0 -0
  14. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/CITATION.cff +0 -0
  15. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/CLAUDE.md +0 -0
  16. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/CONTRIBUTING.md +0 -0
  17. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/LICENSE +0 -0
  18. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/Makefile +0 -0
  19. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/README.md +0 -0
  20. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/beamformer.m +0 -0
  21. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu_cuda124 +0 -0
  22. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/debug_solver_bin/fullwave2_3d_exponential_attenuation_multi_gpu_cuda124 +0 -0
  23. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/experiments/__init__.py +0 -0
  24. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/experiments/exp_scatterer_diameter.py +0 -0
  25. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/beamformer/__init__.py +0 -0
  26. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/beamformer/beamformer.py +0 -0
  27. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/constants/__init__.py +0 -0
  28. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/constants/material_properties.py +0 -0
  29. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/grid.py +0 -0
  30. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium.py +0 -0
  31. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/__init__.py +0 -0
  32. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/domain.py +0 -0
  33. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/medium_builder.py +0 -0
  34. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/__init__.py +0 -0
  35. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/data/.keep +0 -0
  36. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat +0 -0
  37. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_abdominal_wall.py +0 -0
  38. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_background.py +0 -0
  39. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_scatterer.py +0 -0
  40. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_simple.py +0 -0
  41. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_water_gel.py +0 -0
  42. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/sensor.py +0 -0
  43. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/__init__.py +0 -0
  44. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/binary_manager.py +0 -0
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/cuda_utils.py +0 -0
  55. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/input_file_writer.py +0 -0
  56. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/launcher.py +0 -0
  57. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/pml_builder.py +0 -0
  58. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/solver/utils.py +0 -0
  59. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/source.py +0 -0
  60. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/transducer.py +0 -0
  61. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/check_functions.py +0 -0
  62. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/coordinates.py +0 -0
  63. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/memory_tempfile.py +0 -0
  64. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/numerical.py +0 -0
  65. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/plot_utils.py +0 -0
  66. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/pulse.py +0 -0
  67. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/relaxation_parameters.py +0 -0
  68. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/scatterer.py +0 -0
  69. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/fullwave/utils/signal_process.py +0 -0
  70. {fullwave25-1.2.6.dev1 → fullwave25-1.2.6.dev2}/ruff.toml +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "1.2.6-dev1"
2
+ current_version = "1.2.6-dev2"
3
3
  parse = """(?x)
4
4
  (?P<major>0|[1-9]\\d*)\\.
5
5
  (?P<minor>0|[1-9]\\d*)\\.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fullwave25
3
- Version: 1.2.6.dev1
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-dev1"
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
- return result
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-dev1" # Update via bump-my-version, not manually
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"
@@ -735,7 +735,7 @@ wheels = [
735
735
 
736
736
  [[package]]
737
737
  name = "fullwave25"
738
- version = "1.2.6.dev1"
738
+ version = "1.2.6.dev2"
739
739
  source = { editable = "." }
740
740
  dependencies = [
741
741
  { name = "joblib" },
File without changes