k-Wave-python 0.6.1__py3-none-any.whl → 0.6.2__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: k-Wave-python
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: Acoustics toolbox for time domain acoustic and ultrasound simulations in complex and tissue-realistic media.
5
5
  Project-URL: Homepage, http://www.k-wave.org/
6
6
  Project-URL: Documentation, https://waltersimson.com/k-wave-python/
@@ -179,31 +179,32 @@ Classifier: Operating System :: OS Independent
179
179
  Classifier: Programming Language :: Python :: 3
180
180
  Requires-Python: >=3.10
181
181
  Requires-Dist: beartype==0.22.9
182
- Requires-Dist: deepdiff==8.6.2
182
+ Requires-Dist: deepdiff==9.0.0
183
183
  Requires-Dist: deprecated>=1.2.14
184
- Requires-Dist: h5py==3.15.1
185
- Requires-Dist: jaxtyping==0.3.2
186
- Requires-Dist: matplotlib==3.10.7
184
+ Requires-Dist: h5py==3.16.0
185
+ Requires-Dist: jaxtyping==0.3.7
186
+ Requires-Dist: matplotlib==3.10.9
187
187
  Requires-Dist: numpy<2.3.0,>=1.22.2
188
188
  Requires-Dist: opencv-python==4.13.0.92
189
189
  Requires-Dist: scipy==1.15.3
190
190
  Requires-Dist: tqdm>=4.60
191
191
  Provides-Extra: dev
192
- Requires-Dist: pre-commit==4.5.1; extra == 'dev'
192
+ Requires-Dist: pre-commit==4.6.0; extra == 'dev'
193
193
  Provides-Extra: docs
194
- Requires-Dist: furo==2024.8.6; extra == 'docs'
194
+ Requires-Dist: furo==2025.12.19; extra == 'docs'
195
195
  Requires-Dist: sphinx-copybutton==0.5.2; extra == 'docs'
196
196
  Requires-Dist: sphinx-mdinclude==0.6.2; extra == 'docs'
197
- Requires-Dist: sphinx-tabs==3.4.7; extra == 'docs'
198
- Requires-Dist: sphinx-toolbox==3.8.0; extra == 'docs'
197
+ Requires-Dist: sphinx-tabs==3.4.5; extra == 'docs'
198
+ Requires-Dist: sphinx-toolbox==4.1.2; extra == 'docs'
199
+ Requires-Dist: sphinx<9; extra == 'docs'
199
200
  Provides-Extra: example
200
- Requires-Dist: gdown==5.2.0; extra == 'example'
201
+ Requires-Dist: gdown==6.0.0; extra == 'example'
201
202
  Provides-Extra: test
202
- Requires-Dist: coverage==7.10.6; extra == 'test'
203
+ Requires-Dist: coverage==7.14.0; extra == 'test'
203
204
  Requires-Dist: phantominator; extra == 'test'
204
205
  Requires-Dist: pytest; extra == 'test'
205
206
  Requires-Dist: pytest-xdist; extra == 'test'
206
- Requires-Dist: requests==2.33.0; extra == 'test'
207
+ Requires-Dist: requests==2.34.0; extra == 'test'
207
208
  Requires-Dist: testfixtures==8.3.0; extra == 'test'
208
209
  Description-Content-Type: text/markdown
209
210
 
@@ -1,23 +1,23 @@
1
- kwave/__init__.py,sha256=fT86NYPN8sEBD2O91DMdswhNuVcnye2JPj0ZjId61Bg,7008
2
- kwave/compat.py,sha256=KscnU7yPF4jLfEj9PIeQdIiIn8eQGDgtsTsL6sE9l9U,2923
1
+ kwave/__init__.py,sha256=zoHNedvJIeMioBhOiLFVCGvfhFYX3L2YYHCL3jjllxk,9674
2
+ kwave/compat.py,sha256=2qr4XhKTNbB-RZjg6pYpUnnkObUyR78maMNsJKuzL3A,3158
3
3
  kwave/data.py,sha256=WA0bMA7ScwW3HMyt15GkQOfWctcguFN8NHwALiXwO2I,2623
4
- kwave/enums.py,sha256=tptEv0ZDcXEd2k5S-3cM21YudmagMQm2oYx52MwZDPk,470
4
+ kwave/enums.py,sha256=lF8RV6gn5IAFuT38CMUc5dakHSwAOaqHtmuvyYkXdk4,738
5
5
  kwave/executor.py,sha256=LqCJ8speh6R26p0ezaWEktwWEhA9Q75sJydDwRT-oqU,6367
6
6
  kwave/kWaveSimulation.py,sha256=0M61nrIpf_k1BLq9-3bPkj0xN4jQ3MPPjX1_-LSsnfc,68030
7
- kwave/kgrid.py,sha256=WYICT1g6KES5Gr8SMZG47Wc-mKn-uzg-DsmEECae61I,23197
8
- kwave/kmedium.py,sha256=CS3deWUuyVksC3D5L_Z6UlS7_-0VJX5oDsTCVKUUc4E,9046
7
+ kwave/kgrid.py,sha256=RPCfqpwHcV7jZ2qgrG_nOMu42jnNERIWM9axq5FH8qY,23226
8
+ kwave/kmedium.py,sha256=ZLnUGbkn23EJotJh2oggam4Q4K-fFMzL2yHpM6JbTeA,9953
9
9
  kwave/ksensor.py,sha256=_7KAQK8uCJmFwqxhg-Lio8ojGrU-9on634JnZ11wXmY,4269
10
10
  kwave/ksource.py,sha256=twIQ774Zo7mP8yYqhSVl2PW6Y-cp3TSvYtAt6QN2_iw,19596
11
- kwave/kspaceFirstOrder.py,sha256=c73hZUd5f3qFyzAwExbewGO6CAmLvXJQaWI2lMm_3AU,12017
12
- kwave/kspaceFirstOrder2D.py,sha256=nA-dN3zEsZy4QazxsMBNa5DcBeFjudrh5lIeBAukMSU,16551
13
- kwave/kspaceFirstOrder3D.py,sha256=tWB4KvDRflYZ8rqVpwwtdiCfq7AJ87PGJ-n1qv_QkH8,17463
11
+ kwave/kspaceFirstOrder.py,sha256=aFu-JKeuTuVAYvz3deLzo63-XF6gJrytmR7dXN3om5U,15754
12
+ kwave/kspaceFirstOrder2D.py,sha256=w401i3bq_K8oRXopGWIF5CKISyqWRar6B4m9BiMeFH4,16741
13
+ kwave/kspaceFirstOrder3D.py,sha256=D-nWqLTX_c9ky7OqACkOKJrYfKJ09jFundSMDITUKyk,17653
14
14
  kwave/kspaceFirstOrderAS.py,sha256=NR-RaUD96cfoy3rw3eELkDZfeF4s3vakcM5TP4pIroc,17515
15
15
  kwave/kspaceLineRecon.py,sha256=hSw7ZTxDRPzsS_gk4zeBwk1wefIybEsRbuK0JDY0JxA,5727
16
16
  kwave/kspacePlaneRecon.py,sha256=-0anuLC5IUCFx3drvEKdpzUMFb0lCFBrPeni2QyS8r0,5879
17
17
  kwave/ktransducer.py,sha256=ypcNWmrQxGa0MXjOuyktvZgzs-7wFobS1fGobNi2GfU,31413
18
18
  kwave/recorder.py,sha256=SaDaL1dsnYpsDZ7wfqCWrX0xz8_mtavtpvL8WpbKLAE,5307
19
19
  kwave/kWaveSimulation_helper/__init__.py,sha256=pvq2XTaC-yTWSl96AA2h3Iam-wbs5EAaYGDd4ckyTqs,619
20
- kwave/kWaveSimulation_helper/create_absorption_variables.py,sha256=7Du_Q3Jzr_RiSjhEHF-afdQL_Q5Dds1Xj0Sc-Fmivqg,4926
20
+ kwave/kWaveSimulation_helper/create_absorption_variables.py,sha256=W_jsWquHRCLM5TVF95LG3WkWxwrc2228ki7jF5s92f8,4915
21
21
  kwave/kWaveSimulation_helper/display_simulation_params.py,sha256=ROqXjd4MDlJRZZ_ZrNZ3zL7_Ido_gA9dowTwMs1X_lw,3310
22
22
  kwave/kWaveSimulation_helper/expand_grid_matrices.py,sha256=qTkKQDQhifbE59iejxKlWx6BvZad83SuU7w3X0HA_Rs,11237
23
23
  kwave/kWaveSimulation_helper/retract_transducer_grid_size.py,sha256=jLLAtsRMDi8lVF6SOfkNj7MxBtfhXQZ1AeObSnIkN84,945
@@ -32,15 +32,15 @@ kwave/reconstruction/beamform.py,sha256=JjdYqV-ge2HfQpnR6QwoyQkF3WdbuowTXblchOuj
32
32
  kwave/reconstruction/time_reversal.py,sha256=77O9ZS-ZpRCV4Z0ZIm41qzwGGsbnsP4x688qRhEpIz0,6258
33
33
  kwave/reconstruction/tools.py,sha256=-F9k7Sbc2qvun1cygc4sHVZdFKP4zaD2p_B4aN_efWQ,2397
34
34
  kwave/solvers/__init__.py,sha256=-cfkyK8i0lhgyJx5p-JyK9_5XLVdIdBnjsdQ_Y0AHGw,176
35
- kwave/solvers/cpp_simulation.py,sha256=mCnsDszHJIJAH-pZA4UDL0QPB-O3V-JY_R8NyBlB050,15060
36
- kwave/solvers/kspace_solver.py,sha256=uonTccGA5Otfz0spc2FvKMkyMzTazkAjhNjQ8ik0xH4,35380
35
+ kwave/solvers/cpp_simulation.py,sha256=XAZhFz71BBPRmmAbdOJzQ4QsbwUidkAtdW3ee6TfSeM,16768
36
+ kwave/solvers/kspace_solver.py,sha256=6AjIpkpw08197QNjnE3Io_481nc9GwHLQGZYbjSajkU,41488
37
37
  kwave/solvers/native.py,sha256=lQoCOPss1kedTpT6BIN5A22XtKdVlSNRuowB_61QqaA,1152
38
38
  kwave/solvers/validation.py,sha256=7Zn_EqmDqceD4rTGccHwsjFJIImPXfcDqTwj-nPa7vI,5062
39
39
  kwave/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
40
  kwave/utils/angular_spectrum.py,sha256=ciEYcM_Vv-CdKdCzwjMxL5NVpCvmjEzYh7LYoelw0p4,15220
41
41
  kwave/utils/angular_spectrum_cw.py,sha256=XgENMdku_5gEY2SLXOqyLs6NarkGIPZZW6GbvjJ0mjQ,11319
42
42
  kwave/utils/atten_comp.py,sha256=GGH0UBJQu_QG8K_NpyR2Ouwb0CORWZ3LqRTOF7DWrLA,11446
43
- kwave/utils/checks.py,sha256=VUdPHGKUP9D81-NZk8XgJFGotlG8gOcw9XMryQ5BGJc,9979
43
+ kwave/utils/checks.py,sha256=zKFnM4XJU2McJgMIIK5GvfXxDIPzryrkx86AlVb-2BY,12788
44
44
  kwave/utils/colormap.py,sha256=avgY4Gj2ozswvy_cW3w6rCGbpMiz2vkSgaCINSvz0zE,2440
45
45
  kwave/utils/conversion.py,sha256=E3qQbEdac0j1VSUGqPiRcxwds6NNcBmSZ3DHv9b8ZcU,17683
46
46
  kwave/utils/data.py,sha256=rMZCDT1USQxtiPssvABw4fTxArxgypwh7uLWSrqHIOs,5758
@@ -49,17 +49,17 @@ kwave/utils/filters.py,sha256=eXcvOQyY2Rgn4Azszg0SRuk9pENyB6EshGs7JuIsYDA,22836
49
49
  kwave/utils/interp.py,sha256=3n4TtNiL2YVuDk0Mixe_CPpQIsA3tQ2cjPuJOcfh5F8,15326
50
50
  kwave/utils/io.py,sha256=FAPU-2wgxkkFNEDS5ONC2s_iD6w2lEOf3LwTGlMa8-w,16631
51
51
  kwave/utils/kwave_array.py,sha256=uxQGWvsESuhI5Yl62qus4hbEGtcLE0Xp6v5ZjZnnLj0,39505
52
- kwave/utils/mapgen.py,sha256=kHghj_mxsGFMW1cPBw4cgmDbw-IcgLko0Fpnkdwry9Y,116073
53
- kwave/utils/math.py,sha256=fKS1g7XvOs6qQ6R5Ykss1hu9vant1VbemuBcNuiQBT0,13843
52
+ kwave/utils/mapgen.py,sha256=Gzwzr_oT1RPegFLtlrPct3kX1xfxd8ffpebJBmjK3jE,115847
53
+ kwave/utils/math.py,sha256=zzbOEbl-M1RQxVY7xwJYfDn3VPgDda7L6RaeUZ6evwk,13810
54
54
  kwave/utils/matlab.py,sha256=FUN8aVQ1eewC4tgMjcbynxj-kp1jyWxXpXEiNS-iXTM,5655
55
55
  kwave/utils/matrix.py,sha256=RFQYJ7As7sLRj6iW1hoci0xxDvBoKibtYQ5J4nrcSkQ,14120
56
- kwave/utils/plot.py,sha256=MU6Pl4GYIZttePNfky1bEqdFyDb1W1_omlAtWfbd3b8,1496
56
+ kwave/utils/plot.py,sha256=xDPa41z7JJKMN9jDpLJqiYEsHRcp7MmcLk3v3CqRP5I,1496
57
57
  kwave/utils/pml.py,sha256=ijlwgxwXbBgl1V7x5B-r9stqIXOj-DEnJBsO5y-Wl5M,6282
58
58
  kwave/utils/sharpness_filters.py,sha256=mG0oiOVuKJB6Dn_d-44Vrf-48JmYxr6XN4uBmIC9Ji8,3799
59
59
  kwave/utils/signals.py,sha256=R4FNEc6RWKzkJoiPaALdiEcxZqwkAJx7Dd1UDv_HgBI,28767
60
60
  kwave/utils/tictoc.py,sha256=lHAsEspcfw2AzYBgiOQgeUOEjuKMJHf8D7wcbl4wev0,1426
61
- kwave/utils/typing.py,sha256=uu_dCUIlHiyov4avdOZ2BfloOuFiXzQwzQ51tixcIZc,1028
62
- k_wave_python-0.6.1.dist-info/METADATA,sha256=fPhqtF7JuR1pig23dOxlEdCXgNrxw6o8eO4tdZIes3s,13392
63
- k_wave_python-0.6.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
64
- k_wave_python-0.6.1.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
65
- k_wave_python-0.6.1.dist-info/RECORD,,
61
+ kwave/utils/typing.py,sha256=s2_yfomloEkTxQy6cvvSuSI1o7e5DvdUa3nGpnyZQUs,1562
62
+ k_wave_python-0.6.2.dist-info/METADATA,sha256=6WZri4_-b-BBgnWsal5A2FF2oMSxdcYoTQLdH1dQFEI,13435
63
+ k_wave_python-0.6.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
64
+ k_wave_python-0.6.2.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
65
+ k_wave_python-0.6.2.dist-info/RECORD,,
kwave/__init__.py CHANGED
@@ -3,23 +3,45 @@ import json
3
3
  import logging
4
4
  import os
5
5
  import platform
6
+ import stat
7
+ import warnings
6
8
  from pathlib import Path
7
9
  from typing import List
8
10
  from urllib.request import urlretrieve
9
11
 
10
12
  # Test installation with:
11
13
  # python3 -m pip install -i https://test.pypi.org/simple/ --extra-index-url=https://pypi.org/simple/ k-Wave-python==0.3.0
12
- __version__ = "0.6.1"
14
+ __version__ = "0.6.2"
13
15
 
14
16
  # Constants and Configurations
15
17
  URL_BASE = "https://github.com/waltsims/"
16
- BINARY_VERSION = "v1.3.0"
17
- PREFIX = f"{URL_BASE}kspaceFirstOrder-{{}}-{{}}/releases/download/{BINARY_VERSION}/"
18
+ BINARY_VERSION = "v1.4.1"
19
+ # Pin both Windows binaries to v1.3.0. v1.4.x windows builds switched compiler /
20
+ # OpenMP / FFT / CUDA runtime stacks and neither v1.4.x release ships its runtime
21
+ # DLLs (cufft, cudart, vcomp, vcruntime140_1, fftw3f, etc.). v1.3.0 binaries are
22
+ # self-contained with their Intel-era DLL bundle (listed in WINDOWS_DLLS below,
23
+ # downloaded with the OMP request and used by both .exe files since they share
24
+ # kwave/bin/windows/). OMP DLL bundling is fixed in kspacefirstorder-unified#14
25
+ # (awaiting validation); CUDA DLL bundling is tracked in kspacefirstorder-unified#17.
26
+ WINDOWS_OMP_VERSION = "v1.3.0"
27
+ WINDOWS_CUDA_VERSION = "v1.3.0"
18
28
  PLATFORM = platform.system().lower()
19
29
 
20
30
  if PLATFORM not in ["linux", "windows", "darwin"]:
21
31
  raise NotImplementedError(f"k-wave-python is currently unsupported on this operating system: {PLATFORM}.")
22
32
 
33
+ # darwin C++ binary is arm64-only; universal2 coverage tracked for v0.6.5
34
+ DARWIN_BINARY_ARCH = "arm64"
35
+ _darwin_unsupported = PLATFORM == "darwin" and platform.machine() != DARWIN_BINARY_ARCH
36
+ if _darwin_unsupported:
37
+ warnings.warn(
38
+ f"k-wave-python's macOS C++ binary is {DARWIN_BINARY_ARCH}-only. "
39
+ f"Detected {platform.machine()} — the C++ backend (backend='cpp') will not run on this machine. "
40
+ "Use backend='python' instead. Universal2 (Intel + Apple Silicon) coverage is tracked for v0.6.5.",
41
+ RuntimeWarning,
42
+ stacklevel=2,
43
+ )
44
+
23
45
  # TODO: install directly in to /bin/ directory system directory is no longer needed
24
46
  # TODO: deprecate in 0.5.0
25
47
  BINARY_PATH = Path(__file__).parent / "bin" / PLATFORM
@@ -44,21 +66,24 @@ ARCHITECTURES = ["omp", "cuda"]
44
66
 
45
67
 
46
68
  def get_windows_release_urls(architecture: str) -> list:
69
+ version = WINDOWS_OMP_VERSION if architecture == "omp" else WINDOWS_CUDA_VERSION
47
70
  specific_filenames = [EXECUTABLE_PREFIX + architecture + ".exe"]
48
71
  if architecture == "omp":
49
72
  specific_filenames += WINDOWS_DLLS
50
- release_urls = [PREFIX.format(architecture.upper(), PLATFORM.lower()) + filename for filename in specific_filenames]
51
- return release_urls
73
+ base = f"{URL_BASE}kspaceFirstOrder-{architecture.upper()}-{PLATFORM.lower()}/releases/download/{version}/"
74
+ return [base + filename for filename in specific_filenames]
52
75
 
53
76
 
54
77
  URL_DICT = {
55
78
  "linux": {
56
- "cuda": [URL_BASE + f"kspaceFirstOrder-CUDA-{PLATFORM}/releases/download/v1.3.1/{EXECUTABLE_PREFIX}CUDA"],
79
+ "cuda": [URL_BASE + f"kspaceFirstOrder-CUDA-{PLATFORM}/releases/download/{BINARY_VERSION}/{EXECUTABLE_PREFIX}CUDA"],
57
80
  "omp": [URL_BASE + f"kspaceFirstOrder-OMP-{PLATFORM}/releases/download/{BINARY_VERSION}/{EXECUTABLE_PREFIX}OMP"],
58
81
  },
59
82
  "darwin": {
60
83
  "cuda": [],
61
- "omp": [URL_BASE + f"k-wave-omp-{PLATFORM}/releases/download/v0.3.0rc3/{EXECUTABLE_PREFIX}OMP"],
84
+ "omp": (
85
+ [] if _darwin_unsupported else [URL_BASE + f"k-wave-omp-{PLATFORM}/releases/download/{BINARY_VERSION}/{EXECUTABLE_PREFIX}OMP"]
86
+ ),
62
87
  },
63
88
  "windows": {architecture: get_windows_release_urls(architecture) for architecture in ARCHITECTURES},
64
89
  }
@@ -77,6 +102,31 @@ def _hash_file(filepath: str) -> str:
77
102
  return md5.hexdigest()
78
103
 
79
104
 
105
+ def _ensure_executable(binary_filepath) -> None:
106
+ # Self-heal the executable bit on Linux/macOS. urlretrieve creates files
107
+ # at 0644, and prior versions of this package didn't fix that up, so users
108
+ # upgrading with a cached non-executable binary on disk would otherwise
109
+ # stay stuck (the cache check below returns True and skips re-download).
110
+ # Any OS-level failure here (broken symlink, read-only FS, wrong ownership,
111
+ # TOCTOU race) is degraded to a warning so it never aborts `import kwave`.
112
+ if PLATFORM == "windows":
113
+ return
114
+ try:
115
+ current_mode = os.stat(binary_filepath).st_mode
116
+ desired_mode = current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
117
+ if current_mode == desired_mode:
118
+ return
119
+ os.chmod(binary_filepath, desired_mode)
120
+ except OSError: # pragma: no cover - defensive; degrades to warning, never fatal
121
+ # Don't abort import. The user can chmod +x manually or reinstall
122
+ # into a writable location.
123
+ logging.warning(
124
+ "kwave: cannot set executable bit on %s — backend='cpp' may fail with "
125
+ "Permission denied. Run `chmod +x` manually or reinstall.",
126
+ binary_filepath,
127
+ )
128
+
129
+
80
130
  def _is_binary_present(binary_name: str, binary_type: str) -> bool:
81
131
  binary_filepath = BINARY_PATH / binary_name
82
132
  binary_file_exists = os.path.exists(binary_filepath)
@@ -106,6 +156,8 @@ def _is_binary_present(binary_name: str, binary_type: str) -> bool:
106
156
  if existing_metadata["url"] not in latest_urls:
107
157
  return False
108
158
 
159
+ _ensure_executable(binary_filepath)
160
+
109
161
  # No need to check `version` field for now
110
162
  # because we version is already present in the URL
111
163
  return True
@@ -176,6 +228,7 @@ def download_binaries(system_os: str, bin_type: str):
176
228
  try:
177
229
  binary_filepath = os.path.join(BINARY_PATH, filename)
178
230
  urlretrieve(url, binary_filepath)
231
+ _ensure_executable(binary_filepath)
179
232
  _record_binary_metadata(binary_version=binary_version, binary_filepath=binary_filepath, binary_url=url, filename=filename)
180
233
 
181
234
  except TimeoutError:
kwave/compat.py CHANGED
@@ -70,5 +70,9 @@ def options_to_kwargs(simulation_options=None, execution_options=None):
70
70
  kwargs["num_threads"] = opts.num_threads
71
71
  if opts.device_num is not None:
72
72
  kwargs["device_num"] = opts.device_num
73
+ # Read _binary_path directly: the property auto-resolves to a default,
74
+ # so it can't distinguish a user-set path from one.
75
+ if opts._binary_path is not None:
76
+ kwargs["binary_path"] = opts._binary_path
73
77
 
74
78
  return kwargs
kwave/enums.py CHANGED
@@ -1,5 +1,17 @@
1
1
  from enum import Enum
2
2
 
3
+
4
+ class AlphaMode(str, Enum):
5
+ """Controls which absorption/dispersion terms are included in the equation of state."""
6
+
7
+ NO_ABSORPTION = "no_absorption"
8
+ NO_DISPERSION = "no_dispersion"
9
+ STOKES = "stokes"
10
+
11
+ def __str__(self):
12
+ return self.value
13
+
14
+
3
15
  ################################################################
4
16
  # literals that link the discrete cosine and sine transform types with
5
17
  # their type definitions in the functions dtt1D, dtt2D, and dtt3D
@@ -12,19 +12,19 @@ def create_absorption_variables(kgrid: kWaveGrid, medium: kWaveMedium, equation_
12
12
  # define the lossy derivative operators and proportionality coefficients
13
13
  """
14
14
  Selects and returns absorption and dispersion operators and coefficients for the given medium based on the equation of state.
15
-
15
+
16
16
  Parameters:
17
17
  kgrid (kWaveGrid): Grid object providing wavenumber array via `kgrid.k`.
18
18
  medium (kWaveMedium): Medium properties used to compute absorption/dispersion coefficients.
19
19
  equation_of_state (str): One of `"absorbing"`, `"stokes"`, or `"lossless"` determining which variables to produce.
20
-
20
+
21
21
  Returns:
22
22
  tuple: (nabla1, nabla2, tau, eta)
23
23
  - nabla1: First-order absorption operator or `None` when not applicable.
24
24
  - nabla2: Dispersion operator or `None` when not applicable.
25
25
  - tau: Absorbing coefficient or `None` when not applicable.
26
26
  - eta: Dispersive coefficient or `None` when not applicable.
27
-
27
+
28
28
  Behavior:
29
29
  - "absorbing": returns (nabla1, nabla2, tau, eta) computed for an absorbing medium.
30
30
  - "stokes": returns (None, None, tau, None) where `tau` is the Stokes absorbing coefficient.
@@ -127,4 +127,4 @@ def apply_alpha_filter(medium, nabla1, nabla2):
127
127
  # shift the parameters back
128
128
  nabla1 = np.fft.ifftshift(nabla1)
129
129
  nabla2 = np.fft.ifftshift(nabla2)
130
- return nabla1, nabla2
130
+ return nabla1, nabla2
kwave/kgrid.py CHANGED
@@ -108,7 +108,7 @@ class kWaveGrid(object):
108
108
  @t_array.setter
109
109
  def t_array(self, t_array):
110
110
  # check for 'auto' input
111
- if t_array == "auto":
111
+ if isinstance(t_array, str) and t_array == "auto":
112
112
  # set values to auto
113
113
  self.Nt = "auto"
114
114
  self.dt = "auto"
kwave/kmedium.py CHANGED
@@ -1,50 +1,65 @@
1
1
  import logging
2
2
  from dataclasses import dataclass
3
- from typing import List
3
+ from typing import List, Optional, Sequence, Union
4
4
 
5
5
  import numpy as np
6
6
 
7
- import kwave.utils.checks
7
+ from kwave.enums import AlphaMode
8
+
9
+
10
+ def _to_alpha_mode(value):
11
+ """Normalize a value to AlphaMode. Accepts None, AlphaMode, or a valid string."""
12
+ if value is None or isinstance(value, AlphaMode):
13
+ return value
14
+ try:
15
+ return AlphaMode(value)
16
+ except (ValueError, TypeError):
17
+ raise ValueError(
18
+ f"medium.alpha_mode must be an AlphaMode enum value or one of 'no_absorption', 'no_dispersion', 'stokes', got {value!r}"
19
+ ) from None
8
20
 
9
21
 
10
22
  @dataclass
11
23
  class kWaveMedium(object):
24
+ """
25
+ Medium properties for k-Wave simulations.
26
+
27
+ Note: For heterogeneous medium parameters, medium.sound_speed and medium.density
28
+ must be given in matrix form with the same dimensions as kgrid. For homogeneous
29
+ medium parameters, these can be given as single numeric values. If the medium is
30
+ homogeneous and velocity inputs or outputs are not required, it is not necessary
31
+ to specify medium.density.
32
+ """
33
+
12
34
  # sound speed distribution within the acoustic medium [m/s] | required to be defined
13
- sound_speed: np.array
35
+ sound_speed: Union[float, int, np.ndarray]
14
36
  # reference sound speed used within the k-space operator (phase correction term) [m/s]
15
- sound_speed_ref: np.array = None
37
+ sound_speed_ref: Optional[Union[float, int, np.ndarray]] = None
16
38
  # density distribution within the acoustic medium [kg/m^3]
17
- density: np.array = None
39
+ density: Optional[Union[float, int, np.ndarray]] = None
18
40
  # power law absorption coefficient [dB/(MHz^y cm)]
19
- alpha_coeff: np.array = None
41
+ alpha_coeff: Optional[Union[float, int, np.ndarray]] = None
20
42
  # power law absorption exponent
21
- alpha_power: np.array = None
43
+ alpha_power: Optional[Union[float, int, np.ndarray]] = None
22
44
  # optional input to force either the absorption or dispersion terms in the equation of state to be excluded;
23
- # valid inputs are 'no_absorption' or 'no_dispersion'
24
- alpha_mode: np.array = None
45
+ # valid inputs are AlphaMode.NO_ABSORPTION, AlphaMode.NO_DISPERSION, or the equivalent strings
46
+ alpha_mode: Optional[Union[AlphaMode, str]] = None
25
47
  # frequency domain filter applied to the absorption and dispersion terms in the equation of state
26
- alpha_filter: np.array = None
48
+ alpha_filter: Optional[np.ndarray] = None
27
49
  # two element array used to control the sign of absorption and dispersion terms in the equation of state
28
- alpha_sign: np.array = None
50
+ alpha_sign: Optional[np.ndarray] = None
29
51
  # parameter of nonlinearity
30
- BonA: np.array = None
52
+ BonA: Optional[Union[float, int, np.ndarray]] = None
31
53
  # is the medium absorbing?
32
54
  absorbing: bool = False
33
55
  # is the medium absorbing stokes?
34
56
  stokes: bool = False
35
57
 
36
- # """
37
- # Note: For heterogeneous medium parameters, medium.sound_speed and
38
- # medium.density must be given in matrix form with the same dimensions as
39
- # kgrid. For homogeneous medium parameters, these can be given as single
40
- # numeric values. If the medium is homogeneous and velocity inputs or
41
- # outputs are not required, it is not necessary to specify medium.density.
42
- # """
43
-
44
58
  def __post_init__(self):
45
59
  self.sound_speed = np.atleast_1d(self.sound_speed)
60
+ self.alpha_mode = _to_alpha_mode(self.alpha_mode)
46
61
 
47
- def check_fields(self, kgrid_shape: np.ndarray) -> None:
62
+ def check_fields(self, kgrid_shape: Sequence[int]) -> None:
48
63
  """
49
64
  Check whether the given properties are valid
50
65
 
@@ -54,26 +69,21 @@ class kWaveMedium(object):
54
69
  Returns:
55
70
  None
56
71
  """
57
- # check the absorption mode input is valid
58
- if self.alpha_mode is not None:
59
- assert self.alpha_mode in [
60
- "no_absorption",
61
- "no_dispersion",
62
- "stokes",
63
- ], "medium.alpha_mode must be set to 'no_absorption', 'no_dispersion', or 'stokes'."
72
+ # re-normalize alpha_mode in case it was reassigned as a plain string post-construction
73
+ self.alpha_mode = _to_alpha_mode(self.alpha_mode)
64
74
 
65
75
  # check the absorption filter input is valid
66
- if self.alpha_filter is not None and not (self.alpha_filter.shape == kgrid_shape).all():
76
+ if self.alpha_filter is not None and self.alpha_filter.shape != tuple(kgrid_shape):
67
77
  raise ValueError("medium.alpha_filter must be the same size as the computational grid.")
68
78
 
69
79
  # check the absorption sign input is valid
70
- if self.alpha_sign is not None and (not kwave.utils.checkutils.is_number(self.alpha_sign) or (self.alpha_sign.size != 2)):
71
- raise ValueError(
72
- "medium.alpha_sign must be given as a " "2 element numerical array controlling absorption and dispersion, respectively."
73
- )
80
+ if self.alpha_sign is not None:
81
+ alpha_sign_arr = np.atleast_1d(self.alpha_sign)
82
+ if alpha_sign_arr.size != 2 or not np.issubdtype(alpha_sign_arr.dtype, np.number):
83
+ raise ValueError("medium.alpha_sign must be a 2 element numeric array controlling absorption and dispersion, respectively.")
74
84
 
75
85
  # check alpha_coeff is non-negative and real
76
- if not np.all(np.isreal(self.alpha_coeff)) or np.any(self.alpha_coeff < 0):
86
+ if self.alpha_coeff is not None and (not np.all(np.isreal(self.alpha_coeff)) or np.any(self.alpha_coeff < 0)):
77
87
  raise ValueError("medium.alpha_coeff must be non-negative and real.")
78
88
 
79
89
  def is_defined(self, *fields) -> List[bool]:
@@ -102,7 +112,7 @@ class kWaveMedium(object):
102
112
  None
103
113
  """
104
114
  for f in fields:
105
- assert getattr(self, f) is not None, f"The field {f} must be not be None"
115
+ assert getattr(self, f) is not None, f"The field {f} must not be None"
106
116
 
107
117
  def is_nonlinear(self) -> bool:
108
118
  """
@@ -141,13 +151,14 @@ class kWaveMedium(object):
141
151
  # enforce both absorption parameters
142
152
  self.ensure_defined("alpha_coeff", "alpha_power")
143
153
 
144
- # check y is a scalar
145
- assert np.isscalar(self.alpha_power), "medium.alpha_power must be scalar."
154
+ # check y is a scalar (np.isscalar rejects 0-d ndarrays — accept np.array(1.5) too)
155
+ is_scalar = np.isscalar(self.alpha_power) or (isinstance(self.alpha_power, np.ndarray) and self.alpha_power.size == 1)
156
+ assert is_scalar, "medium.alpha_power must be scalar."
146
157
 
147
158
  # check y is real and within 0 to 3
148
- assert (
149
- np.all(np.isreal(self.alpha_coeff)) and 0 <= self.alpha_power < 3
150
- ), "medium.alpha_power must be a real number between 0 and 3."
159
+ assert np.all(np.isreal(self.alpha_coeff)) and 0 <= self.alpha_power < 3, (
160
+ "medium.alpha_power must be a real number between 0 and 3."
161
+ )
151
162
 
152
163
  # display warning if y is close to 1 and the dispersion term has not been set to zero
153
164
  if self.alpha_mode != "no_dispersion":
@@ -167,8 +178,10 @@ class kWaveMedium(object):
167
178
  self.ensure_defined("alpha_coeff")
168
179
 
169
180
  # give warning if y is specified
170
- if self.alpha_power is not None and (self.alpha_power.size != 1 or self.alpha_power != 2):
171
- logging.log(logging.WARN, "the axisymmetric code and stokes absorption assume alpha_power = 2, user value ignored.")
181
+ if self.alpha_power is not None:
182
+ ap = np.asarray(self.alpha_power)
183
+ if ap.size != 1 or not np.isclose(ap.item(), 2.0):
184
+ logging.warning("the axisymmetric code and stokes absorption assume alpha_power = 2, user value ignored.")
172
185
 
173
186
  # overwrite y value
174
187
  self.alpha_power = 2
@@ -176,12 +189,12 @@ class kWaveMedium(object):
176
189
  # don't allow medium.alpha_mode with the axisymmetric code
177
190
  if self.alpha_mode is not None and (self.alpha_mode in ["no_absorption", "no_dispersion"]):
178
191
  raise NotImplementedError(
179
- "Input option medium.alpha_mode is not supported with the axisymmetric code " "or medium.alpha_mode = " "stokes" "."
192
+ "Input option medium.alpha_mode is not supported with the axisymmetric code or medium.alpha_mode = stokes."
180
193
  )
181
194
 
182
195
  # don't allow alpha_filter with stokes absorption (no variables are applied in k-space)
183
196
  assert self.alpha_filter is None, (
184
- "Input option medium.alpha_filter is not supported with the axisymmetric code " "or medium.alpha_mode = 'stokes'. "
197
+ "Input option medium.alpha_filter is not supported with the axisymmetric code or medium.alpha_mode = 'stokes'. "
185
198
  )
186
199
 
187
200
  ##########################################
kwave/kspaceFirstOrder.py CHANGED
@@ -82,6 +82,40 @@ def _strip_pml(result, pml_size, ndim, suffixes=_FULL_GRID_SUFFIXES):
82
82
  }
83
83
 
84
84
 
85
+ def _resolve_dtype(value):
86
+ """Normalize a dtype-like input to ``np.float32`` or ``np.float64``.
87
+
88
+ Accepts numpy dtypes/types (``np.float32``, ``np.float64``), strings
89
+ (``"float32"`` etc., plus MATLAB aliases ``"single"`` / ``"double"``),
90
+ Python ``float``, ``None`` (default → float64), and the legacy MATLAB
91
+ ``"off"`` alias for float64. Anything that resolves to a non-float32 /
92
+ non-float64 dtype raises ``ValueError`` — the solver isn't validated
93
+ for ``float16`` / complex dtypes.
94
+
95
+ Cupy dtypes (``cp.float32``, ``cp.float64``) work for free because cupy
96
+ re-exports numpy's scalar types. Torch / JAX dtypes are not accepted —
97
+ they live in different ecosystems and don't translate via ``np.dtype()``;
98
+ the error message points the user at the equivalent numpy dtype.
99
+ """
100
+ if value is None or value == "off":
101
+ return np.float64
102
+ try:
103
+ resolved = np.dtype(value).type
104
+ except TypeError as e:
105
+ framework = getattr(type(value), "__module__", "").split(".")[0]
106
+ hint = ""
107
+ if framework in ("torch", "jax", "jaxlib", "tensorflow"):
108
+ hint = f" {framework}.dtype objects aren't supported; pass the equivalent numpy dtype (np.float32 / np.float64)."
109
+ raise ValueError(
110
+ f"dtype must be a numpy dtype, type, or string (e.g. 'float32', 'single'), got {value!r}.{hint}"
111
+ ) from e
112
+ if resolved is np.float32:
113
+ return np.float32
114
+ if resolved is np.float64:
115
+ return np.float64
116
+ raise ValueError(f"dtype must resolve to float32 or float64; got {resolved.__name__} from {value!r}")
117
+
118
+
85
119
  def kspaceFirstOrder(
86
120
  kgrid: kWaveGrid,
87
121
  medium: kWaveMedium,
@@ -96,12 +130,14 @@ def kspaceFirstOrder(
96
130
  smooth_p0: bool = True,
97
131
  backend: str = "python",
98
132
  device: str = "cpu",
133
+ dtype=None,
99
134
  save_only: bool = False,
100
135
  data_path: Optional[str] = None,
101
136
  quiet: bool = False,
102
137
  debug: bool = False,
103
138
  num_threads: Optional[int] = None,
104
139
  device_num: Optional[int] = None,
140
+ binary_path: Optional[str] = None,
105
141
  ) -> dict:
106
142
  """Run a k-Wave simulation.
107
143
 
@@ -142,6 +178,24 @@ def kspaceFirstOrder(
142
178
  device: ``"cpu"`` or ``"gpu"``. For ``backend="python"`` this
143
179
  selects NumPy (cpu) vs CuPy (gpu). For ``backend="cpp"`` it
144
180
  selects the OMP vs CUDA binary. Default ``"cpu"``.
181
+ dtype: Numerical precision for state arrays in the Python backend.
182
+ Accepts dtype-like input — a numpy dtype (``np.float32``,
183
+ ``np.float64``; cupy aliases like ``cp.float32`` work since cupy
184
+ re-exports numpy's scalar types), a string (``"float32"``,
185
+ ``"float64"``, ``"single"``, ``"double"``), a Python type
186
+ (``float``), or ``None`` for the default (float64). The
187
+ MATLAB-style alias ``"off"`` is accepted as a synonym for
188
+ float64 to ease migration from the legacy
189
+ ``SimulationOptions.data_cast``. Torch / JAX dtypes are not
190
+ accepted; pass the numpy equivalent (e.g. ``np.float32`` for
191
+ ``torch.float32``).
192
+ ``np.float32`` uses roughly half the memory and is faster on
193
+ most hardware, at the cost of reduced numerical accuracy.
194
+ Only ``float32`` and ``float64`` are supported; other dtypes
195
+ raise ``ValueError``. Has no effect on ``backend="cpp"`` (the
196
+ C++ binary uses fixed internal precision regardless); a warning
197
+ is emitted if ``dtype`` resolves to anything other than float64
198
+ with the C++ backend. Default ``None`` (float64).
145
199
  save_only: When ``True`` (``backend="cpp"`` only), write the HDF5
146
200
  input file and return without running the binary. Useful for
147
201
  cluster submission. Default ``False``.
@@ -154,6 +208,9 @@ def kspaceFirstOrder(
154
208
  num_threads: Thread count for the C++ OMP binary. ``None`` uses all
155
209
  available cores. Default ``None``.
156
210
  device_num: GPU device index for CUDA execution. Default ``None``.
211
+ binary_path: Path to a custom C++ binary. When ``None`` (default),
212
+ the binary bundled with ``k-wave-data`` is used. Only applies
213
+ when ``backend="cpp"``.
157
214
 
158
215
  Returns:
159
216
  dict: Recorded sensor data keyed by field name (e.g.
@@ -167,6 +224,7 @@ def kspaceFirstOrder(
167
224
  raise ValueError(f"device must be 'cpu' or 'gpu', got {device!r}")
168
225
  if backend not in ("python", "cpp"):
169
226
  raise ValueError(f"Unknown backend: {backend!r}. Use 'python' or 'cpp'.")
227
+ dtype = _resolve_dtype(dtype)
170
228
 
171
229
  if isinstance(pml_size, str) and pml_size.lower() == "auto":
172
230
  pml_size = tuple(int(x) for x in get_optimal_pml_size(kgrid))
@@ -206,10 +264,23 @@ def kspaceFirstOrder(
206
264
  pml_size=pml_size,
207
265
  pml_alpha=pml_alpha,
208
266
  quiet=quiet,
267
+ dtype=dtype,
209
268
  ).run()
210
269
 
211
270
  elif backend == "cpp":
212
271
  from kwave.solvers.cpp_simulation import CppSimulation
272
+ from kwave.utils.checks import check_alpha_mode_cpp_compatible, warn_alpha_power_near_unity_cpp
273
+
274
+ check_alpha_mode_cpp_compatible(medium)
275
+ warn_alpha_power_near_unity_cpp(medium)
276
+
277
+ if dtype is not np.float64:
278
+ warnings.warn(
279
+ f"dtype={np.dtype(dtype).name!r} has no effect with backend='cpp'; the C++ binary "
280
+ "uses fixed internal precision regardless. Use backend='python' to control "
281
+ "computational precision.",
282
+ stacklevel=2,
283
+ )
213
284
 
214
285
  if not use_kspace:
215
286
  warnings.warn(
@@ -243,7 +314,7 @@ def kspaceFirstOrder(
243
314
  if not pml_inside:
244
315
  result["pml_size"] = pml_size
245
316
  return result
246
- result = cpp_sim.run(device=device, num_threads=num_threads, device_num=device_num, quiet=quiet, debug=debug, data_path=data_path)
317
+ result = cpp_sim.run(device=device, num_threads=num_threads, device_num=device_num, quiet=quiet, debug=debug, data_path=data_path, binary_path=binary_path)
247
318
 
248
319
  # --- Post-processing: strip PML from full-grid fields ---
249
320
 
@@ -217,6 +217,11 @@ def kspaceFirstOrder2D(
217
217
 
218
218
  return run_python_backend(kgrid, medium, source, sensor, simulation_options, execution_options)
219
219
 
220
+ from kwave.utils.checks import check_alpha_mode_cpp_compatible, warn_alpha_power_near_unity_cpp
221
+
222
+ check_alpha_mode_cpp_compatible(medium)
223
+ warn_alpha_power_near_unity_cpp(medium)
224
+
220
225
  # Currently we only support binary execution, meaning all simulations must be saved to disk.
221
226
  if not simulation_options.save_to_disk:
222
227
  if execution_options.is_gpu_simulation: