wavesimpro 1.5.0__tar.gz → 1.6.1__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 (20) hide show
  1. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/PKG-INFO +1 -1
  2. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/pyproject.toml +1 -1
  3. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/__init__.py +1 -1
  4. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/client.py +6 -0
  5. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/execute.py +84 -14
  6. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/json_helper.py +738 -711
  7. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/simulate.py +21 -9
  8. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/validate.py +102 -93
  9. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro.egg-info/PKG-INFO +1 -1
  10. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/setup.cfg +0 -0
  11. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/__main__.py +0 -0
  12. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/binary_utils.py +0 -0
  13. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/config.py +0 -0
  14. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/exceptions.py +0 -0
  15. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/py.typed +0 -0
  16. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro/setup.py +0 -0
  17. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro.egg-info/SOURCES.txt +0 -0
  18. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
  19. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro.egg-info/requires.txt +0 -0
  20. {wavesimpro-1.5.0 → wavesimpro-1.6.1}/src/wavesimpro.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavesimpro
3
- Version: 1.5.0
3
+ Version: 1.6.1
4
4
  Summary: WavesimPro Python API — cloud execution client for the Rayfos simulation platform
5
5
  Author: Rayfos
6
6
  Project-URL: Repository, https://github.com/rayfos/wavesimpro
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wavesimpro"
7
- version = "1.5.0"
7
+ version = "1.6.1"
8
8
  description = "WavesimPro Python API — cloud execution client for the Rayfos simulation platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -45,7 +45,7 @@ from .json_helper import (
45
45
  parse_progress_data,
46
46
  )
47
47
 
48
- __version__ = "1.5.0"
48
+ __version__ = "1.6.1"
49
49
  __all__ = [
50
50
  # Client
51
51
  "RayfosClient",
@@ -285,6 +285,7 @@ class RayfosClient:
285
285
  periodic: tuple = (False, False, False),
286
286
  engine_preference: str = "Any",
287
287
  use_gpu: Optional[bool] = None,
288
+ is_vectorial: Optional[bool] = None,
288
289
  ) -> Dict[str, Any]:
289
290
  """
290
291
  Ask the server (cloud or machine-service) whether a simulation of the
@@ -301,6 +302,10 @@ class RayfosClient:
301
302
  periodic: (periodic_x, periodic_y, periodic_z) — periodic axes don't get boundary padding
302
303
  engine_preference: "Any" | "Python" | "Cpp"
303
304
  use_gpu: None (auto), True (require GPU), False (force CPU)
305
+ is_vectorial: True = Maxwell (vectorial, 3 field components), False =
306
+ Helmholtz (scalar). Memory differs ~3x between the two, so ALWAYS
307
+ pass it: this payload carries no sources, and without the flag the
308
+ server falls back to "Maxwell unless engine_preference is Python".
304
309
 
305
310
  Returns:
306
311
  PreflightCheckResponse dict with keys:
@@ -339,6 +344,7 @@ class RayfosClient:
339
344
  },
340
345
  "enginePreference": engine_preference,
341
346
  "useGpu": use_gpu,
347
+ "isVectorial": is_vectorial,
342
348
  }
343
349
  # Machine service URL takes precedence (local single-machine preflight); else cloud aggregator.
344
350
  if self.config.machine_service_url:
@@ -15,7 +15,7 @@ import shutil
15
15
  import logging
16
16
  import tempfile
17
17
  from pathlib import Path
18
- from typing import Tuple, Optional, Callable, Dict, Any, List, Union
18
+ from typing import Tuple, Optional, Callable, Dict, Any, List, Union, Sequence
19
19
 
20
20
  import numpy as np
21
21
 
@@ -61,6 +61,37 @@ def _monitor_output_filename(monitor_name: str) -> str:
61
61
  return f"monitor_{core}.bin"
62
62
 
63
63
 
64
+ def _normalize_boundary_widths(boundary_widths) -> Optional[List[Tuple[float, float]]]:
65
+ """Normalize a per-axis boundary spec into three (left, right) pairs in µm.
66
+
67
+ Accepts a sequence of exactly 3 entries (x, y, z); each entry is either a
68
+ single number (same width on both sides of that axis) or a (left, right)
69
+ pair. Returns None when boundary_widths is None.
70
+ """
71
+ if boundary_widths is None:
72
+ return None
73
+ if len(boundary_widths) != 3:
74
+ raise ValueError(
75
+ "boundary_widths must have exactly 3 entries (x, y, z); each entry is "
76
+ "a width in MICROMETERS or a (left, right) pair, e.g. "
77
+ "boundary_widths=[6.0, (6.0, 0.0), 6.0]"
78
+ )
79
+ out: List[Tuple[float, float]] = []
80
+ for axis, w in zip("xyz", boundary_widths):
81
+ if isinstance(w, (int, float)):
82
+ left = right = float(w)
83
+ else:
84
+ if len(w) != 2:
85
+ raise ValueError(
86
+ f"boundary_widths[{axis}] must be a number or a (left, right) pair, got {w!r}"
87
+ )
88
+ left, right = float(w[0]), float(w[1])
89
+ if left < 0 or right < 0:
90
+ raise ValueError(f"boundary_widths[{axis}] sides must be >= 0, got ({left}, {right})")
91
+ out.append((left, right))
92
+ return out
93
+
94
+
64
95
  def execute_via_api(
65
96
  refractive_index: np.ndarray,
66
97
  source,
@@ -68,7 +99,9 @@ def execute_via_api(
68
99
  pixel_size: float,
69
100
  threshold: float = 1e-2,
70
101
  max_iterations: int = 1000,
71
- boundary_width: Optional[float] = None, # in MICROMETERS; REQUIRED — None raises; no silent default
102
+ boundary_width: Optional[float] = None, # in MICROMETERS; REQUIRED unless boundary_widths is given
103
+ boundary_widths: Optional[Sequence[Any]] = None, # per-axis [x, y, z] µm; each a number or (left, right)
104
+ boundary_strength: Optional[float] = None, # None = engine default 1.0 (C++ engine only)
72
105
  is_vectorial: bool = True,
73
106
  data_type: str = "RefractiveIndex",
74
107
  api_url: Optional[str] = None,
@@ -113,6 +146,18 @@ def execute_via_api(
113
146
  BREAKING CHANGE: in earlier versions this parameter was in PIXELS
114
147
  and defaulted to 0. If you are migrating, pass your old pixel
115
148
  count × pixel_size instead.
149
+ boundary_widths: OPTIONAL asymmetric boundaries (C++ engine, 2026-09
150
+ machine service+). Sequence of exactly 3 entries (x, y, z) in
151
+ MICROMETERS; each entry is a single number (same width on both
152
+ sides of that axis) or a (left, right) pair, e.g.
153
+ ``boundary_widths=[6.0, (6.0, 0.0), 6.0]``. When given,
154
+ boundary_width becomes optional (it is then only the symmetric
155
+ fallback stored as BoundarySize for older servers, defaulting to
156
+ the X-left value). Jobs that resolve to the Python engine collapse
157
+ to a single scalar (the X-left value) with a machine-side warning.
158
+ boundary_strength: OPTIONAL largest imaginary permittivity the
159
+ absorbing ramp lerps towards (C++ engine only). None (default)
160
+ = engine default 1.0. Older servers ignore it.
116
161
  is_vectorial: True for Maxwell, False for Helmholtz (default: True)
117
162
  data_type: 'RefractiveIndex' or 'Permittivity' (default: 'RefractiveIndex')
118
163
  api_url: API base URL (default: from RAYFOS_API_URL env var)
@@ -149,16 +194,26 @@ def execute_via_api(
149
194
  each shaped per its own sub-cube.
150
195
  - converged: True if simulation completed successfully
151
196
  """
197
+ boundary_sides_um = _normalize_boundary_widths(boundary_widths)
152
198
  if boundary_width is None:
153
- raise ValueError(
154
- "boundary_width is required and has no default: the right value depends "
155
- "on the wavelength. It is the absorbing boundary width in MICROMETERS "
156
- "(μm) — typically 4–5 × the vacuum wavelength. Thinner boundaries "
157
- "reflect grazing/guided waves back into the domain; thicker ones "
158
- "increase memory (the boundary pads both sides of every non-periodic "
159
- "axis). NOTE: in earlier versions this parameter was in PIXELS and "
160
- "defaulted to 0 — if migrating, pass old_pixel_count * pixel_size."
161
- )
199
+ if boundary_sides_um is not None:
200
+ # Symmetric fallback stored as BoundarySize: used by pre-2026-09
201
+ # servers (which ignore the per-side fields) and by the Python
202
+ # engine (single scalar). X-axis left, per the degradation rule.
203
+ boundary_width = boundary_sides_um[0][0]
204
+ else:
205
+ raise ValueError(
206
+ "boundary_width is required and has no default: the right value depends "
207
+ "on the wavelength. It is the absorbing boundary width in MICROMETERS "
208
+ "(μm) — typically 4–5 × the vacuum wavelength. Thinner boundaries "
209
+ "reflect grazing/guided waves back into the domain; thicker ones "
210
+ "increase memory (the boundary pads both sides of every non-periodic "
211
+ "axis). NOTE: in earlier versions this parameter was in PIXELS and "
212
+ "defaulted to 0 — if migrating, pass old_pixel_count * pixel_size. "
213
+ "For asymmetric boundaries pass boundary_widths instead."
214
+ )
215
+ if boundary_strength is not None and boundary_strength <= 0:
216
+ raise ValueError(f"boundary_strength must be > 0 (None = engine default 1.0), got {boundary_strength}")
162
217
 
163
218
  # 1. Load credentials
164
219
  api_url = api_url or os.environ.get("RAYFOS_API_URL", "http://localhost:5159")
@@ -198,7 +253,13 @@ def execute_via_api(
198
253
  logger.info(f" Max iterations: {max_iterations}")
199
254
  if pseudo_propagation_distance > 0:
200
255
  logger.info(f" Pseudo propagation distance: {pseudo_propagation_distance} um (overrides max iterations)")
201
- logger.info(f" Boundary width: {boundary_width} um ({int(round(boundary_width / pixel_size))} pixels)")
256
+ if boundary_sides_um is None:
257
+ logger.info(f" Boundary width: {boundary_width} um ({int(round(boundary_width / pixel_size))} pixels)")
258
+ else:
259
+ logger.info(" Boundary widths (um): " + " ".join(
260
+ f"{ax}:[{left:g}, {right:g}]" for ax, (left, right) in zip("XYZ", boundary_sides_um)))
261
+ if boundary_strength is not None:
262
+ logger.info(f" Boundary strength: {boundary_strength}")
202
263
 
203
264
  # 3. Prepare electromagnetic data
204
265
  if data_type == "RefractiveIndex":
@@ -261,13 +322,18 @@ def execute_via_api(
261
322
  domain_y_um = float(ny) * float(pixel_size)
262
323
  domain_z_um = float(nz) * float(pixel_size)
263
324
  try:
325
+ # The preflight endpoint takes one scalar width; with asymmetric
326
+ # boundaries use the largest side so the estimate stays conservative.
327
+ preflight_boundary_um = (float(boundary_width) if boundary_sides_um is None
328
+ else max(side for pair in boundary_sides_um for side in pair))
264
329
  pre = client.preflight_check(
265
330
  domain_um=(domain_x_um, domain_y_um, domain_z_um),
266
331
  voxel_um=float(pixel_size),
267
- boundary_um=float(boundary_width),
332
+ boundary_um=preflight_boundary_um,
268
333
  periodic=(bool(periodic[0]), bool(periodic[1]), bool(periodic[2])),
269
334
  engine_preference=engine_preference,
270
335
  use_gpu=use_gpu,
336
+ is_vectorial=is_vectorial,
271
337
  )
272
338
  except Exception as exc:
273
339
  logger.warning(f"Preflight check failed (server may not support /api/preflightCheck yet): {exc}. Proceeding without it.")
@@ -277,9 +343,10 @@ def execute_via_api(
277
343
  status = (pre.get("status") or "").lower()
278
344
  pre_description = pre.get("description") or ""
279
345
  est = pre.get("estimate") or {}
346
+ solver = est.get("solver") or ("Maxwell" if est.get("isVectorial") else "Helmholtz")
280
347
  logger.info(
281
348
  f"Preflight: status={status} "
282
- f"est_total={est.get('totalMemoryGB', 0):.2f} GB "
349
+ f"est_total={est.get('totalMemoryGB', 0):.2f} GB ({solver}) "
283
350
  f"(E-field {est.get('efieldMemoryGB', 0):.2f} GB + workspace {est.get('workspaceMemoryGB', 0):.2f} GB) "
284
351
  f"grid={est.get('nx')}x{est.get('ny')}x{est.get('nz')}"
285
352
  )
@@ -326,6 +393,7 @@ def execute_via_api(
326
393
  gpu_memory_max_gb=gpu_memory_max_gb,
327
394
  periodic=periodic,
328
395
  pseudo_propagation_distance=pseudo_propagation_distance,
396
+ data_type=data_type,
329
397
  )
330
398
 
331
399
  # 8. Upload RI/permittivity
@@ -435,6 +503,8 @@ def execute_via_api(
435
503
  wavelength=float(wavelength),
436
504
  voxel_size=float(pixel_size),
437
505
  boundary_size_um=boundary_size_um,
506
+ boundary_sides_um=boundary_sides_um,
507
+ boundary_strength=boundary_strength,
438
508
  residual_norm_threshold=float(threshold),
439
509
  residual_norm_max_iterations=int(max_iterations),
440
510
  periodic_x=bool(periodic[0]),