wavesimpro 1.5.0__tar.gz → 1.6.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavesimpro
3
- Version: 1.5.0
3
+ Version: 1.6.0
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.0"
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.0"
49
49
  __all__ = [
50
50
  # Client
51
51
  "RayfosClient",
@@ -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,10 +322,14 @@ 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,
@@ -435,6 +500,8 @@ def execute_via_api(
435
500
  wavelength=float(wavelength),
436
501
  voxel_size=float(pixel_size),
437
502
  boundary_size_um=boundary_size_um,
503
+ boundary_sides_um=boundary_sides_um,
504
+ boundary_strength=boundary_strength,
438
505
  residual_norm_threshold=float(threshold),
439
506
  residual_norm_max_iterations=int(max_iterations),
440
507
  periodic_x=bool(periodic[0]),
@@ -559,6 +559,8 @@ def create_wavesim_properties(
559
559
  wavelength_unit: str = "Micrometers",
560
560
  voxel_unit: str = "Micrometers",
561
561
  boundary_size_um: float = 0.0,
562
+ boundary_sides_um: Optional[List[Tuple[float, float]]] = None,
563
+ boundary_strength: Optional[float] = None,
562
564
  residual_norm_threshold: float = 1.0e-6,
563
565
  residual_norm_max_iterations: int = 10000,
564
566
  periodic_x: bool = False,
@@ -585,7 +587,13 @@ def create_wavesim_properties(
585
587
  voxel_size: Voxel size value
586
588
  wavelength_unit: Wavelength unit (default: 'Micrometers')
587
589
  voxel_unit: Voxel size unit (default: 'Micrometers')
588
- boundary_size_um: Boundary size in micrometers
590
+ boundary_size_um: Boundary size in micrometers (symmetric fallback)
591
+ boundary_sides_um: Optional three (left, right) pairs in micrometers
592
+ (x, y, z) → emitted as BoundarySizesX/Y/Z for asymmetric
593
+ boundaries. None = symmetric (boundary_size_um everywhere).
594
+ boundary_strength: Optional largest imaginary permittivity of the
595
+ absorbing ramp → emitted as BoundaryStrength. None = engine
596
+ default 1.0. C++ engine only.
589
597
  residual_norm_threshold: Convergence threshold
590
598
  residual_norm_max_iterations: Max iterations
591
599
  periodic_x, periodic_y, periodic_z: Periodic boundary conditions
@@ -594,7 +602,7 @@ def create_wavesim_properties(
594
602
  residual_norm_max_iterations (threshold still stops the run early).
595
603
  C++ engine only.
596
604
  """
597
- return {
605
+ props = {
598
606
  "SimulationDomain": simulation_domain,
599
607
  "BackgroundMediumId": background_medium_id,
600
608
  "Primitives": primitives,
@@ -629,6 +637,20 @@ def create_wavesim_properties(
629
637
  }
630
638
  }
631
639
 
640
+ # Asymmetric per-side boundary widths (BoundarySides on the C# model).
641
+ # Omitted entirely when symmetric so old servers see the same JSON as before.
642
+ if boundary_sides_um is not None:
643
+ for key, (left, right) in zip(("BoundarySizesX", "BoundarySizesY", "BoundarySizesZ"),
644
+ boundary_sides_um):
645
+ props[key] = {
646
+ "Left": {"Value": float(left), "Unit": "Micrometers"},
647
+ "Right": {"Value": float(right), "Unit": "Micrometers"},
648
+ }
649
+ if boundary_strength is not None:
650
+ props["BoundaryStrength"] = float(boundary_strength)
651
+
652
+ return props
653
+
632
654
 
633
655
  def parse_progress_data(progress_obj: Optional[Dict[str, Any]]) -> Dict[str, Any]:
634
656
  """
@@ -97,7 +97,9 @@ def simulate(
97
97
  sources: Sequence[tuple],
98
98
  wavelength: float,
99
99
  pixel_size: float,
100
- boundary_width: float, # μm — REQUIRED, no default: the right value depends on the wavelength
100
+ boundary_width: Optional[float] = None, # μm — REQUIRED unless boundary_widths is given
101
+ boundary_widths=None, # per-axis [x, y, z] µm; each a number or (left, right) — asymmetric boundaries
102
+ boundary_strength=None, # None = engine default 1.0 (C++ engine only)
101
103
  periodic: tuple = (False, False, False), # forwarded to PeriodicX/Y/Z in JSON
102
104
  use_gpu=None, # forwarded to API (None = auto)
103
105
  n_domains=None, # local only — accepted and ignored
@@ -212,6 +214,8 @@ def simulate(
212
214
  wavelength=wavelength,
213
215
  pixel_size=pixel_size,
214
216
  boundary_width=boundary_width, # μm end-to-end; execute_via_api takes μm
217
+ boundary_widths=boundary_widths,
218
+ boundary_strength=boundary_strength,
215
219
  is_vectorial=is_vectorial,
216
220
  data_type="Permittivity",
217
221
  max_iterations=max_iterations,
@@ -246,7 +250,9 @@ def simulate_pro(
246
250
  sources: Sequence[tuple],
247
251
  wavelength: float,
248
252
  pixel_size: float,
249
- boundary_width: float, # μm — REQUIRED, no default: the right value depends on the wavelength
253
+ boundary_width: Optional[float] = None, # μm — REQUIRED unless boundary_widths is given
254
+ boundary_widths=None, # per-axis [x, y, z] µm; each a number or (left, right) — asymmetric boundaries
255
+ boundary_strength=None, # None = engine default 1.0 (C++ engine only)
250
256
  periodic: tuple = (False, False, False),
251
257
  max_iterations: int = 100000,
252
258
  threshold: float = 1.0e-6,
@@ -355,6 +361,8 @@ def simulate_pro(
355
361
  wavelength=wavelength,
356
362
  pixel_size=pixel_size,
357
363
  boundary_width=boundary_width, # μm end-to-end; execute_via_api takes μm
364
+ boundary_widths=boundary_widths,
365
+ boundary_strength=boundary_strength,
358
366
  is_vectorial=is_vectorial,
359
367
  data_type="Permittivity",
360
368
  max_iterations=max_iterations,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavesimpro
3
- Version: 1.5.0
3
+ Version: 1.6.0
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
File without changes