wavesimpro 1.6.5__tar.gz → 1.9.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.
Files changed (23) hide show
  1. wavesimpro-1.9.0/PKG-INFO +69 -0
  2. wavesimpro-1.9.0/README.md +46 -0
  3. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/pyproject.toml +1 -1
  4. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/__init__.py +3 -1
  5. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/client.py +12 -2
  6. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/execute.py +51 -18
  7. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/json_helper.py +127 -6
  8. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/simulate.py +11 -7
  9. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/validate.py +46 -21
  10. wavesimpro-1.9.0/src/wavesimpro.egg-info/PKG-INFO +69 -0
  11. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro.egg-info/SOURCES.txt +1 -0
  12. wavesimpro-1.6.5/PKG-INFO +0 -22
  13. wavesimpro-1.6.5/src/wavesimpro.egg-info/PKG-INFO +0 -22
  14. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/setup.cfg +0 -0
  15. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/__main__.py +0 -0
  16. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/binary_utils.py +0 -0
  17. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/config.py +0 -0
  18. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/exceptions.py +0 -0
  19. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/py.typed +0 -0
  20. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro/setup.py +0 -0
  21. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
  22. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro.egg-info/requires.txt +0 -0
  23. {wavesimpro-1.6.5 → wavesimpro-1.9.0}/src/wavesimpro.egg-info/top_level.txt +0 -0
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: wavesimpro
3
+ Version: 1.9.0
4
+ Summary: WavesimPro Python API — cloud execution client for the Rayfos simulation platform
5
+ Author: Rayfos
6
+ Project-URL: Repository, https://github.com/rayfos/wavesimpro
7
+ Keywords: wavesim,simulation,electromagnetic,rayfos,api
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: numpy>=1.20.0
16
+ Requires-Dist: requests>=2.28.0
17
+ Requires-Dist: python-dotenv>=1.0.0
18
+ Requires-Dist: tqdm>=4.60.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
21
+ Requires-Dist: black>=23.0.0; extra == "dev"
22
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
23
+
24
+ # wavesimpro
25
+
26
+ Python client for the Rayfos Wavesim cloud: build a simulation setup, run it on a Rayfos
27
+ compute node, and read the results back as NumPy arrays.
28
+
29
+ Wavesim is a single-wavelength frequency-domain solver (vectorial Maxwell when a polarized
30
+ source is present, scalar Helmholtz otherwise). This package talks to the Rayfos JobServer
31
+ REST API; it does not contain the solver.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install wavesimpro
37
+ python -m wavesimpro setup # one-time: API key + server URL wizard
38
+ ```
39
+
40
+ The wizard stores the credentials; `wavesimpro.configure(api_key=..., api_url=...)` sets
41
+ them for one process instead.
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ import wavesimpro as wp
47
+
48
+ props = wp.create_wavesim_properties(
49
+ wavelength=1.0, pixel_size=0.1, boundary_width=5.0,
50
+ domain=wp.create_simulation_domain(20, 20, 20),
51
+ mediums=[wp.create_medium("Silicon", 3.48, 0.0)],
52
+ primitives=[wp.create_box_primitive("core", 0, 0, 0, 10, 0.5, 20, medium="Silicon")],
53
+ sources=[wp.create_mode_source("in", 0, 0, -8.0, polarization="X")],
54
+ monitors=[wp.create_monitor("out", 0, 0, 8.0, 10, 10, 0)],
55
+ )
56
+ run = wp.simulate(props, name="strip waveguide")
57
+ field = wp.read_binary_field(run.download("output.bin"))
58
+ ```
59
+
60
+ ## What the API covers
61
+
62
+ - Setup helpers for domains, media, primitives (box, STL, voxel arrays), sources
63
+ (point, plane wave, Gaussian beam, custom field, engine-solved waveguide mode) and monitors.
64
+ - Per-axis pixel sizes (`pixel_size=[dx, dy, dz]`) and sub-pixel permittivity averaging
65
+ (`subpixel_factor=`), when the compute node supports them.
66
+ - Preflight memory check and cost estimate before a run.
67
+ - Binary upload/download helpers for permittivity volumes, source fields and monitors.
68
+
69
+ See `CHANGELOG.md` for what each version added. Issues and feature requests: support@rayfos.com.
@@ -0,0 +1,46 @@
1
+ # wavesimpro
2
+
3
+ Python client for the Rayfos Wavesim cloud: build a simulation setup, run it on a Rayfos
4
+ compute node, and read the results back as NumPy arrays.
5
+
6
+ Wavesim is a single-wavelength frequency-domain solver (vectorial Maxwell when a polarized
7
+ source is present, scalar Helmholtz otherwise). This package talks to the Rayfos JobServer
8
+ REST API; it does not contain the solver.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install wavesimpro
14
+ python -m wavesimpro setup # one-time: API key + server URL wizard
15
+ ```
16
+
17
+ The wizard stores the credentials; `wavesimpro.configure(api_key=..., api_url=...)` sets
18
+ them for one process instead.
19
+
20
+ ## Quick start
21
+
22
+ ```python
23
+ import wavesimpro as wp
24
+
25
+ props = wp.create_wavesim_properties(
26
+ wavelength=1.0, pixel_size=0.1, boundary_width=5.0,
27
+ domain=wp.create_simulation_domain(20, 20, 20),
28
+ mediums=[wp.create_medium("Silicon", 3.48, 0.0)],
29
+ primitives=[wp.create_box_primitive("core", 0, 0, 0, 10, 0.5, 20, medium="Silicon")],
30
+ sources=[wp.create_mode_source("in", 0, 0, -8.0, polarization="X")],
31
+ monitors=[wp.create_monitor("out", 0, 0, 8.0, 10, 10, 0)],
32
+ )
33
+ run = wp.simulate(props, name="strip waveguide")
34
+ field = wp.read_binary_field(run.download("output.bin"))
35
+ ```
36
+
37
+ ## What the API covers
38
+
39
+ - Setup helpers for domains, media, primitives (box, STL, voxel arrays), sources
40
+ (point, plane wave, Gaussian beam, custom field, engine-solved waveguide mode) and monitors.
41
+ - Per-axis pixel sizes (`pixel_size=[dx, dy, dz]`) and sub-pixel permittivity averaging
42
+ (`subpixel_factor=`), when the compute node supports them.
43
+ - Preflight memory check and cost estimate before a run.
44
+ - Binary upload/download helpers for permittivity volumes, source fields and monitors.
45
+
46
+ See `CHANGELOG.md` for what each version added. Issues and feature requests: support@rayfos.com.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wavesimpro"
7
- version = "1.6.5"
7
+ version = "1.9.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"
@@ -37,6 +37,7 @@ from .json_helper import (
37
37
  create_point_source,
38
38
  create_plane_wave_source,
39
39
  create_gaussian_beam_source,
40
+ create_mode_source,
40
41
  create_voxels_model_primitive,
41
42
  create_custom_field_source,
42
43
  create_custom_from_file_source,
@@ -45,7 +46,7 @@ from .json_helper import (
45
46
  parse_progress_data,
46
47
  )
47
48
 
48
- __version__ = "1.6.5"
49
+ __version__ = "1.9.0"
49
50
  __all__ = [
50
51
  # Client
51
52
  "RayfosClient",
@@ -81,6 +82,7 @@ __all__ = [
81
82
  "create_point_source",
82
83
  "create_plane_wave_source",
83
84
  "create_gaussian_beam_source",
85
+ "create_mode_source",
84
86
  "create_voxels_model_primitive",
85
87
  "create_custom_field_source",
86
88
  "create_custom_from_file_source",
@@ -280,7 +280,7 @@ class RayfosClient:
280
280
  def preflight_check(
281
281
  self,
282
282
  domain_um: tuple,
283
- voxel_um: float,
283
+ voxel_um, # float, or (dx, dy, dz) µm for a per-axis grid
284
284
  boundary_sides_um: Sequence[Tuple[float, float]],
285
285
  periodic: tuple = (False, False, False),
286
286
  engine_preference: str = "Any",
@@ -329,6 +329,12 @@ class RayfosClient:
329
329
  def _um(v: float) -> Dict[str, Any]:
330
330
  return {"value": float(v), "unit": "Micrometers"}
331
331
 
332
+ voxels = (tuple(float(v) for v in voxel_um)
333
+ if isinstance(voxel_um, (list, tuple)) else (float(voxel_um),) * 3)
334
+ if len(voxels) != 3:
335
+ raise ValueError("voxel_um must be one number or (dx, dy, dz)")
336
+ anisotropic = any(abs(v - voxels[0]) > 1e-6 * max(voxels) for v in voxels)
337
+
332
338
  body = {
333
339
  "wavesimProperties": {
334
340
  "simulationDomain": {
@@ -336,7 +342,11 @@ class RayfosClient:
336
342
  "position": {"x": _um(0.0), "y": _um(0.0), "z": _um(0.0)},
337
343
  "size": {"width": _um(size_x), "height": _um(size_y), "depth": _um(size_z)},
338
344
  },
339
- "voxelSize": _um(voxel_um),
345
+ # voxelSize = the smallest axis (what scalar readers use); voxelSizes carries
346
+ # the per-axis pitch so the estimator sizes the grid the engine will pad.
347
+ "voxelSize": _um(min(voxels)),
348
+ **({"voxelSizes": {"x": _um(voxels[0]), "y": _um(voxels[1]), "z": _um(voxels[2])}}
349
+ if anisotropic else {}),
340
350
  # boundarySize is the symmetric fallback older servers read; the
341
351
  # per-side widths below are what the current estimator uses.
342
352
  "boundarySize": _um(sides[0][0]),
@@ -92,11 +92,37 @@ def _normalize_boundary_widths(boundary_widths) -> Optional[List[Tuple[float, fl
92
92
  return out
93
93
 
94
94
 
95
+ def pixel_sizes_um(pixel_size) -> Tuple[float, float, float]:
96
+ """
97
+ Normalise a pixel size to a per-axis (dx, dy, dz) tuple in micrometres.
98
+
99
+ ``pixel_size`` may be one number (isotropic, the usual case) or three
100
+ ``[dx, dy, dz]`` (anisotropic: coarsen axes the field is band-limited along,
101
+ e.g. the transverse axes of a paraxial beam or a weakly guiding waveguide;
102
+ Python engine only). Every axis must be positive.
103
+ """
104
+ if isinstance(pixel_size, (int, float, np.integer, np.floating)):
105
+ p = float(pixel_size)
106
+ return (p, p, p)
107
+ vals = tuple(float(v) for v in pixel_size)
108
+ if len(vals) != 3:
109
+ raise ValueError(f"pixel_size must be one number or [dx, dy, dz], got {pixel_size!r}")
110
+ if any(v <= 0 for v in vals):
111
+ raise ValueError(f"pixel_size components must be positive, got {vals}")
112
+ return vals # type: ignore[return-value]
113
+
114
+
115
+ def is_anisotropic(pixel_sizes: Sequence[float]) -> bool:
116
+ """True when the three pitches differ beyond float round-off."""
117
+ m = max(pixel_sizes)
118
+ return any(abs(v - pixel_sizes[0]) > 1e-6 * m for v in pixel_sizes)
119
+
120
+
95
121
  def execute_via_api(
96
122
  refractive_index: np.ndarray,
97
123
  source,
98
124
  wavelength: float,
99
- pixel_size: float,
125
+ pixel_size, # float, or [dx, dy, dz] µm (see pixel_sizes_um)
100
126
  threshold: float = 1e-2,
101
127
  max_iterations: int = 1000,
102
128
  boundary_width: Optional[float] = None, # in MICROMETERS; REQUIRED unless boundary_widths is given
@@ -134,7 +160,9 @@ def execute_via_api(
134
160
  - dict with 'Es' (complex field array) and 'position' ([x,y,z] or [x,y,z,pol])
135
161
  - list of such dicts for multiple sources
136
162
  wavelength: Wavelength in micrometers
137
- pixel_size: Pixel size in micrometers
163
+ pixel_size: Pixel size in micrometers — one number, or [dx, dy, dz] for a
164
+ per-axis grid (coarsen axes the field is band-limited along; Python
165
+ engine only, see pixel_sizes_um)
138
166
  threshold: Convergence threshold (default: 1e-2)
139
167
  max_iterations: Max iterations (default: 1000)
140
168
  boundary_width: Absorbing boundary width in MICROMETERS (μm). Required —
@@ -248,13 +276,17 @@ def execute_via_api(
248
276
 
249
277
  logger.info(f" Domain: [{nx}, {ny}, {nz}]")
250
278
  logger.info(f" Wavelength: {wavelength:.3f} um")
251
- logger.info(f" Pixel size: {pixel_size:.3f} um")
279
+ ps = pixel_sizes_um(pixel_size)
280
+ anisotropic = is_anisotropic(ps)
281
+ ps_min = min(ps)
282
+ ps_label = f"({ps[0]:.4g}, {ps[1]:.4g}, {ps[2]:.4g})" if anisotropic else f"{ps[0]:.3f}"
283
+ logger.info(f" Pixel size: {ps_label} um")
252
284
  logger.info(f" Threshold: {threshold:.2e}")
253
285
  logger.info(f" Max iterations: {max_iterations}")
254
286
  if pseudo_propagation_distance > 0:
255
287
  logger.info(f" Pseudo propagation distance: {pseudo_propagation_distance} um (overrides max iterations)")
256
288
  if boundary_sides_um is None:
257
- logger.info(f" Boundary width: {boundary_width} um ({int(round(boundary_width / pixel_size))} pixels)")
289
+ logger.info(f" Boundary width: {boundary_width} um ({int(round(boundary_width / ps_min))} pixels on the finest axis)")
258
290
  else:
259
291
  logger.info(" Boundary widths (um): " + " ".join(
260
292
  f"{ax}:[{left:g}, {right:g}]" for ax, (left, right) in zip("XYZ", boundary_sides_um)))
@@ -318,9 +350,9 @@ def execute_via_api(
318
350
 
319
351
  # 5b. Preflight check — abort BEFORE creating a project / uploading anything
320
352
  # if the simulation won't fit on the available compute targets.
321
- domain_x_um = float(nx) * float(pixel_size)
322
- domain_y_um = float(ny) * float(pixel_size)
323
- domain_z_um = float(nz) * float(pixel_size)
353
+ domain_x_um = float(nx) * ps[0]
354
+ domain_y_um = float(ny) * ps[1]
355
+ domain_z_um = float(nz) * ps[2]
324
356
  # The preflight describes exactly the grid the engine will pad: the same
325
357
  # per-side boundary widths that go into the submitted properties.
326
358
  preflight_sides_um = (boundary_sides_um if boundary_sides_um is not None
@@ -328,7 +360,7 @@ def execute_via_api(
328
360
  try:
329
361
  pre = client.preflight_check(
330
362
  domain_um=(domain_x_um, domain_y_um, domain_z_um),
331
- voxel_um=float(pixel_size),
363
+ voxel_um=ps,
332
364
  boundary_sides_um=preflight_sides_um,
333
365
  periodic=(bool(periodic[0]), bool(periodic[1]), bool(periodic[2])),
334
366
  engine_preference=engine_preference,
@@ -379,7 +411,7 @@ def execute_via_api(
379
411
 
380
412
  # 7. Validate parameters
381
413
  validate_parameters(
382
- em_data, sources_info, wavelength, pixel_size,
414
+ em_data, sources_info, wavelength, ps,
383
415
  threshold, max_iterations, boundary_width, is_vectorial,
384
416
  strict_validation=strict_validation,
385
417
  gpu_memory_max_gb=gpu_memory_max_gb,
@@ -424,9 +456,9 @@ def execute_via_api(
424
456
 
425
457
  # 10. Build WavesimProperties payload
426
458
  logger.info("Building command payload...")
427
- domain_x_um = float(nx) * float(pixel_size)
428
- domain_y_um = float(ny) * float(pixel_size)
429
- domain_z_um = float(nz) * float(pixel_size)
459
+ domain_x_um = float(nx) * ps[0]
460
+ domain_y_um = float(ny) * ps[1]
461
+ domain_z_um = float(nz) * ps[2]
430
462
 
431
463
  simulation_domain = create_simulation_domain(domain_x_um, domain_y_um, domain_z_um)
432
464
  background_medium = create_medium("background", "Background", background_ri)
@@ -435,7 +467,7 @@ def execute_via_api(
435
467
  voxel_primitive = create_voxels_model_primitive(
436
468
  primitive_name, em_data_id, "background",
437
469
  0.0, 0.0, 0.0,
438
- float(pixel_size), float(pixel_size), float(pixel_size),
470
+ ps[0], ps[1], ps[2],
439
471
  int(nx), int(ny), int(nz),
440
472
  "Complex64", data_type,
441
473
  )
@@ -459,9 +491,9 @@ def execute_via_api(
459
491
  # centre, world 0,0,0). Keeping every client emitting the same shape
460
492
  # of WavesimProperties means the server has one coordinate path, not
461
493
  # one per client.
462
- src_extent_x = float(src_nx) * float(pixel_size)
463
- src_extent_y = float(src_ny) * float(pixel_size)
464
- src_extent_z = float(src_nz) * float(pixel_size)
494
+ src_extent_x = float(src_nx) * ps[0]
495
+ src_extent_y = float(src_ny) * ps[1]
496
+ src_extent_z = float(src_nz) * ps[2]
465
497
  position_x_centered = float(src["position"][0]) + src_extent_x / 2.0 - domain_x_um / 2.0
466
498
  position_y_centered = float(src["position"][1]) + src_extent_y / 2.0 - domain_y_um / 2.0
467
499
  position_z_centered = float(src["position"][2]) + src_extent_z / 2.0 - domain_z_um / 2.0
@@ -493,7 +525,8 @@ def execute_via_api(
493
525
  monitors=monitors or [],
494
526
  background_medium_id="background",
495
527
  wavelength=float(wavelength),
496
- voxel_size=float(pixel_size),
528
+ voxel_size=ps_min, # smallest axis: scalar readers over-resolve
529
+ voxel_sizes=list(ps) if anisotropic else None, # per-axis pitch, Python engine only
497
530
  boundary_size_um=boundary_size_um,
498
531
  boundary_sides_um=boundary_sides_um,
499
532
  boundary_strength=boundary_strength,
@@ -534,7 +567,7 @@ def execute_via_api(
534
567
  print(f" Version : {version_id}")
535
568
  print(f" Command : {command_id}")
536
569
  print(f" Mode : {_mode}")
537
- print(f" Domain : {_domain_str} lambda={wavelength} um dx={pixel_size} um")
570
+ print(f" Domain : {_domain_str} lambda={wavelength} um dx={ps_label} um")
538
571
  print(f" Engine : {_engine} | GPU: {_gpu}")
539
572
  if preferred_machine_id:
540
573
  print(f" Machine : {preferred_machine_id}")
@@ -118,10 +118,11 @@ def create_box_primitive(
118
118
  "Y": {"Value": center_y, "Unit": unit},
119
119
  "Z": {"Value": center_z, "Unit": unit}
120
120
  },
121
+ # Size3D on the server is Width/Height/Depth (X/Y/Z keys were silently ignored → 4 µm default).
121
122
  "Size": {
122
- "X": {"Value": size_x, "Unit": unit},
123
- "Y": {"Value": size_y, "Unit": unit},
124
- "Z": {"Value": size_z, "Unit": unit}
123
+ "Width": {"Value": size_x, "Unit": unit},
124
+ "Height": {"Value": size_y, "Unit": unit},
125
+ "Depth": {"Value": size_z, "Unit": unit}
125
126
  }
126
127
  }
127
128
 
@@ -321,10 +322,11 @@ def create_gaussian_beam_source(
321
322
  "Imaginary": amplitude_imag
322
323
  },
323
324
  "SourcePlane": source_plane,
325
+ # Size3D on the server is Width/Height/Depth (X/Y/Z keys were silently ignored → 10 µm default).
324
326
  "Size": {
325
- "X": {"Value": size_x, "Unit": unit},
326
- "Y": {"Value": size_y, "Unit": unit},
327
- "Z": {"Value": size_z, "Unit": unit}
327
+ "Width": {"Value": size_x, "Unit": unit},
328
+ "Height": {"Value": size_y, "Unit": unit},
329
+ "Depth": {"Value": size_z, "Unit": unit}
328
330
  },
329
331
  "Alpha": alpha,
330
332
  "Origin": origin
@@ -397,6 +399,97 @@ def create_voxels_model_primitive(
397
399
  }
398
400
 
399
401
 
402
+ def create_mode_source(
403
+ name: str = "Mode1",
404
+ position_x: float = 0.0,
405
+ position_y: float = 0.0,
406
+ position_z: float = 0.0,
407
+ polarization: str = "None",
408
+ amplitude_real: float = 1.0,
409
+ amplitude_imag: float = 0.0,
410
+ neff_guess: Optional[float] = None,
411
+ solver: str = "Engine",
412
+ file_id: Optional[str] = None,
413
+ size_x: float = 0.0,
414
+ size_y: float = 0.0,
415
+ unit: str = "Micrometers"
416
+ ) -> Dict[str, Any]:
417
+ """
418
+ Create a waveguide Mode source.
419
+
420
+ solver="Engine" (default): the compute node solves the eigenmode of the voxelised
421
+ permittivity cross-section at the source's z when the run starts — the real geometry,
422
+ every primitive and material included — and launches it along +z on the XY plane. Nothing
423
+ is uploaded. The eigenmode closest to ``neff_guess`` is injected; leave it None for the
424
+ fundamental mode (the guess defaults to just below the highest index in the cross-section).
425
+ Runs need a compute node advertising the ``ModeSource:Cpp`` feature (C++ engine); the
426
+ solved n_eff is written to the run's ``modeSources.json`` output.
427
+
428
+ solver="Client": a precomputed mode profile uploaded as a field file (``file_id``), run
429
+ like a CustomField source — the Studio designer's flow.
430
+
431
+ Args:
432
+ name: Source name
433
+ position_x, position_y, position_z: Source position (centre-based). The engine solves on
434
+ the XY plane at z, inside the size_x × size_y window centred on (x, y).
435
+ polarization: "None" (scalar Helmholtz mode), "X" (Ex) or "Y" (Ey). "Z" is not a mode
436
+ field in the XY plane and is rejected for the engine solver.
437
+ amplitude_real, amplitude_imag: Complex amplitude applied to the normalised profile
438
+ neff_guess: Engine solver: effective-index guess; the closest eigenmode is injected
439
+ solver: "Engine" or "Client"
440
+ file_id: Client solver: id of the uploaded mode-profile field file (required)
441
+ size_x, size_y: Engine solver: the solve window around the position (clamped to the
442
+ domain); 0 = the whole cross-section. Client solver: extent of the uploaded profile.
443
+ unit: Unit of measurement
444
+ """
445
+ solver_norm = str(solver).strip().capitalize()
446
+ if solver_norm not in ("Engine", "Client"):
447
+ raise ValueError(f"solver must be 'Engine' or 'Client', got {solver!r}")
448
+ if solver_norm == "Client" and not file_id:
449
+ raise ValueError("solver='Client' needs file_id (the uploaded mode-profile field)")
450
+ if solver_norm == "Engine" and str(polarization) == "Z":
451
+ raise ValueError("an engine-solved mode is 'None' (scalar), 'X' (Ex) or 'Y' (Ey); 'Z' has no mode field in the XY plane")
452
+ if neff_guess is not None and not neff_guess > 0:
453
+ raise ValueError("neff_guess must be positive (or None for automatic)")
454
+
455
+ source: Dict[str, Any] = {
456
+ "Id": str(uuid.uuid4()),
457
+ "SourceType": "Mode",
458
+ "Name": name,
459
+ "IsEnabled": True,
460
+ "Position": {
461
+ "X": {"Value": position_x, "Unit": unit},
462
+ "Y": {"Value": position_y, "Unit": unit},
463
+ "Z": {"Value": position_z, "Unit": unit}
464
+ },
465
+ "Polarization": polarization,
466
+ "Origin": "Center",
467
+ "Amplitude": {
468
+ "Real": amplitude_real,
469
+ "Imaginary": amplitude_imag
470
+ },
471
+ "Solver": solver_norm,
472
+ # The engine works on the XY cross-section and launches along +z; these describe the
473
+ # same plane for the client/designer path and for Studio's drawing.
474
+ "SourcePlane": "xy",
475
+ "Size": {
476
+ "Width": {"Value": size_x, "Unit": unit},
477
+ "Height": {"Value": size_y, "Unit": unit},
478
+ "Depth": {"Value": 0.0, "Unit": unit}
479
+ },
480
+ # Designer bookkeeping Studio expects on a Mode source (not used by the engine solver).
481
+ "WaveguideGeometry": "Rectangular",
482
+ "ModeFamily": "TE",
483
+ "ModeIndex_n": 0,
484
+ "ModeIndex_m": 1,
485
+ }
486
+ if neff_guess is not None:
487
+ source["NeffGuess"] = float(neff_guess)
488
+ if file_id:
489
+ source["FileId"] = file_id
490
+ return source
491
+
492
+
400
493
  def create_custom_field_source(
401
494
  name: str,
402
495
  field_data_id: str,
@@ -564,6 +657,7 @@ def create_wavesim_properties(
564
657
  wavelength_unit: str = "Micrometers",
565
658
  voxel_unit: str = "Micrometers",
566
659
  boundary_size_um: float = 0.0,
660
+ voxel_sizes: Optional[List[float]] = None,
567
661
  boundary_sides_um: Optional[List[Tuple[float, float]]] = None,
568
662
  boundary_strength: Optional[float] = None,
569
663
  residual_norm_threshold: float = 1.0e-6,
@@ -574,6 +668,7 @@ def create_wavesim_properties(
574
668
  alpha: float = 0.75,
575
669
  keep_residuals_list: bool = False,
576
670
  pseudo_propagation_distance: float = 0.0,
671
+ subpixel_factor: Optional[int] = None,
577
672
  ) -> Dict[str, Any]:
578
673
  """
579
674
  Create complete WavesimProperties structure
@@ -654,6 +749,32 @@ def create_wavesim_properties(
654
749
  if boundary_strength is not None:
655
750
  props["BoundaryStrength"] = float(boundary_strength)
656
751
 
752
+ # Per-axis voxel sizes (VoxelSizes3D on the C# model). Omitted when isotropic so
753
+ # old servers see the same JSON as before; when present, VoxelSize above must be
754
+ # the smallest axis (callers pass min(voxel_sizes)) so scalar readers over-resolve.
755
+ if voxel_sizes is not None:
756
+ if len(voxel_sizes) != 3:
757
+ raise ValueError("voxel_sizes must be [dx, dy, dz]")
758
+ props["VoxelSizes"] = {
759
+ axis: {"Value": float(v), "Unit": voxel_unit}
760
+ for axis, v in zip(("X", "Y", "Z"), voxel_sizes)
761
+ }
762
+
763
+ # Sub-pixel permittivity averaging (PermittivitySampling on the C# model). Omitted =
764
+ # Staircase. The service probes each voxel of primitives / STL / GDSII at
765
+ # subpixel_factor points per axis and stores the mean permittivity; it does NOT
766
+ # re-sample a VoxelsModel array (average that yourself before upload).
767
+ if subpixel_factor is not None:
768
+ s = int(subpixel_factor)
769
+ if s < 1 or s > 64:
770
+ raise ValueError("subpixel_factor must be an integer between 1 and 64 (16 is the reference value)")
771
+ props["PermittivitySampling"] = {
772
+ "Mode": "SubPixel" if s > 1 else "Staircase",
773
+ "Factor": s,
774
+ "Rule": "ArithmeticMean",
775
+ "InterfaceOnly": True,
776
+ }
777
+
657
778
  return props
658
779
 
659
780
 
@@ -17,7 +17,7 @@ from typing import Callable, Optional, Sequence
17
17
 
18
18
  import numpy as np
19
19
 
20
- from .execute import execute_via_api
20
+ from .execute import execute_via_api, pixel_sizes_um
21
21
 
22
22
 
23
23
  def _caller_script_name() -> str:
@@ -100,7 +100,7 @@ def simulate(
100
100
  permittivity: np.ndarray,
101
101
  sources: Sequence[tuple],
102
102
  wavelength: float,
103
- pixel_size: float,
103
+ pixel_size, # float µm, or [dx, dy, dz] µm per axis (Python engine only; see pixel_sizes_um)
104
104
  boundary_width: Optional[float] = None, # μm — REQUIRED unless boundary_widths is given
105
105
  boundary_widths=None, # per-axis [x, y, z] µm; each a number or (left, right) — asymmetric boundaries
106
106
  boundary_strength=None, # None = engine default 1.0 (C++ engine only)
@@ -183,15 +183,17 @@ def simulate(
183
183
 
184
184
  is_vectorial = any(len(s[1]) == 4 for s in sources)
185
185
 
186
+ # Source positions are voxel indices; convert per axis (pixel_size may be [dx, dy, dz]).
187
+ dx, dy, dz = pixel_sizes_um(pixel_size)
186
188
  api_sources = []
187
189
  for src_array, src_pos in sources:
188
190
  pos = list(src_pos)
189
191
  if is_vectorial:
190
192
  pol_1based = int(pos[0]) + 1
191
- api_pos = [pos[1] * pixel_size, pos[2] * pixel_size, pos[3] * pixel_size, pol_1based]
193
+ api_pos = [pos[1] * dx, pos[2] * dy, pos[3] * dz, pol_1based]
192
194
  src_field = (src_array[0] if src_array.ndim == 4 else src_array).astype(np.complex64)
193
195
  else:
194
- api_pos = [pos[0] * pixel_size, pos[1] * pixel_size, pos[2] * pixel_size]
196
+ api_pos = [pos[0] * dx, pos[1] * dy, pos[2] * dz]
195
197
  src_field = src_array.astype(np.complex64)
196
198
  api_sources.append({"Es": src_field, "position": api_pos})
197
199
 
@@ -253,7 +255,7 @@ def simulate_pro(
253
255
  permittivity: np.ndarray,
254
256
  sources: Sequence[tuple],
255
257
  wavelength: float,
256
- pixel_size: float,
258
+ pixel_size, # float µm, or [dx, dy, dz] µm per axis (Python engine only; see pixel_sizes_um)
257
259
  boundary_width: Optional[float] = None, # μm — REQUIRED unless boundary_widths is given
258
260
  boundary_widths=None, # per-axis [x, y, z] µm; each a number or (left, right) — asymmetric boundaries
259
261
  boundary_strength=None, # None = engine default 1.0 (C++ engine only)
@@ -330,15 +332,17 @@ def simulate_pro(
330
332
 
331
333
  is_vectorial = any(len(s[1]) == 4 for s in sources)
332
334
 
335
+ # Source positions are voxel indices; convert per axis (pixel_size may be [dx, dy, dz]).
336
+ dx, dy, dz = pixel_sizes_um(pixel_size)
333
337
  api_sources = []
334
338
  for src_array, src_pos in sources:
335
339
  pos = list(src_pos)
336
340
  if is_vectorial:
337
341
  pol_1based = int(pos[0]) + 1
338
- api_pos = [pos[1] * pixel_size, pos[2] * pixel_size, pos[3] * pixel_size, pol_1based]
342
+ api_pos = [pos[1] * dx, pos[2] * dy, pos[3] * dz, pol_1based]
339
343
  src_field = (src_array[0] if src_array.ndim == 4 else src_array).astype(np.complex64)
340
344
  else:
341
- api_pos = [pos[0] * pixel_size, pos[1] * pixel_size, pos[2] * pixel_size]
345
+ api_pos = [pos[0] * dx, pos[1] * dy, pos[2] * dz]
342
346
  src_field = src_array.astype(np.complex64)
343
347
  api_sources.append({"Es": src_field, "position": api_pos})
344
348
 
@@ -43,7 +43,7 @@ def validate_parameters(
43
43
  refractive_index: np.ndarray,
44
44
  sources: List[Dict[str, Any]],
45
45
  wavelength: float,
46
- pixel_size: float,
46
+ pixel_size, # float, or [dx, dy, dz] µm per axis
47
47
  threshold: float = 1e-2,
48
48
  max_iterations: int = 1000,
49
49
  boundary_width: Optional[float] = None,
@@ -179,24 +179,49 @@ def validate_parameters(
179
179
  # 3. Validate pixel size against the wavelength IN THE HIGHEST-INDEX MEDIUM
180
180
  # (lambda_medium = lambda_vacuum / n_max). n_max is the refractive index
181
181
  # — derived as sqrt(eps) when permittivity was passed.
182
+ # pixel_size may be one number or [dx, dy, dz]: every axis must resolve the field.
183
+ # Scalar checks further down (boundary pixels, domain extents) use the finest axis.
184
+ pixel_sizes = None
182
185
  if pixel_size is None:
183
186
  errors.append("Pixel size is required")
184
- elif pixel_size <= 0:
185
- errors.append(f"Pixel size must be positive (got {pixel_size:.4f})")
186
- elif n_max is not None and wavelength and wavelength > 0:
187
- nyquist_limit = wavelength / (2 * n_max)
188
- if pixel_size > nyquist_limit:
189
- warnings.append(
190
- f"Pixel size ({pixel_size:.4f} um) exceeds Nyquist limit "
191
- f"({nyquist_limit:.4f} um for n_max={n_max:.2f}). Results may be inaccurate."
192
- )
193
- pixels_per_wavelength = wavelength / (n_max * pixel_size)
194
- if pixels_per_wavelength < 10:
195
- warnings.append(
196
- f"Only {pixels_per_wavelength:.1f} pixels per wavelength in the "
197
- f"highest-index medium (n_max={n_max:.2f}). Recommend >= 10."
198
- )
199
- logger.info(f" Pixels per wavelength in n_max={n_max:.2f}: {pixels_per_wavelength:.1f}")
187
+ else:
188
+ try:
189
+ pixel_sizes = ((float(pixel_size),) * 3 if not isinstance(pixel_size, (list, tuple, np.ndarray))
190
+ else tuple(float(v) for v in pixel_size))
191
+ except (TypeError, ValueError):
192
+ errors.append(f"Pixel size must be a number or [dx, dy, dz] (got {pixel_size!r})")
193
+ if pixel_sizes is not None and len(pixel_sizes) != 3:
194
+ errors.append(f"Pixel size must be one number or [dx, dy, dz] (got {pixel_size!r})")
195
+ pixel_sizes = None
196
+ if pixel_sizes is not None and any(v <= 0 for v in pixel_sizes):
197
+ errors.append(f"Pixel size must be positive (got {pixel_sizes})")
198
+ pixel_sizes = None
199
+ if pixel_sizes is not None:
200
+ pixel_size = min(pixel_sizes)
201
+ anisotropic = any(abs(v - pixel_sizes[0]) > 1e-6 * max(pixel_sizes) for v in pixel_sizes)
202
+ if anisotropic:
203
+ logger.info(f" Per-axis pixel size: {pixel_sizes} um (Python engine only)")
204
+ if n_max is not None and wavelength and wavelength > 0:
205
+ nyquist_limit = wavelength / (2 * n_max)
206
+ for axis, p in zip("xyz", pixel_sizes):
207
+ label = f"Pixel size {axis} ({p:.4f} um)" if anisotropic else f"Pixel size ({p:.4f} um)"
208
+ if p > nyquist_limit:
209
+ warnings.append(
210
+ f"{label} exceeds Nyquist limit "
211
+ f"({nyquist_limit:.4f} um for n_max={n_max:.2f}). Results may be inaccurate."
212
+ )
213
+ if not anisotropic:
214
+ break
215
+ pixels_per_wavelength = wavelength / (n_max * max(pixel_sizes))
216
+ if pixels_per_wavelength < 10:
217
+ warnings.append(
218
+ f"Only {pixels_per_wavelength:.1f} pixels per wavelength in the "
219
+ f"highest-index medium (n_max={n_max:.2f}) on the coarsest axis. Recommend >= 10 "
220
+ f"unless that axis is deliberately coarse (band-limited field)."
221
+ )
222
+ logger.info(f" Pixels per wavelength in n_max={n_max:.2f} (coarsest axis): {pixels_per_wavelength:.1f}")
223
+ else:
224
+ pixel_size = None
200
225
 
201
226
  # 4. Validate threshold
202
227
  if threshold is None:
@@ -319,10 +344,10 @@ def validate_parameters(
319
344
  pos = src.get("position")
320
345
  if pos is None or len(pos) < 3:
321
346
  errors.append(f"Source {i + 1}: position must have at least 3 elements [x, y, z]")
322
- elif pixel_size and pixel_size > 0:
323
- domain_x = nx * pixel_size
324
- domain_y = ny * pixel_size
325
- domain_z = nz * pixel_size
347
+ elif pixel_sizes is not None:
348
+ domain_x = nx * pixel_sizes[0]
349
+ domain_y = ny * pixel_sizes[1]
350
+ domain_z = nz * pixel_sizes[2]
326
351
  if pos[0] < 0 or pos[0] > domain_x:
327
352
  warnings.append(f"Source {i + 1}: x position ({pos[0]:.2f} um) outside domain [0, {domain_x:.2f}]")
328
353
  if pos[1] < 0 or pos[1] > domain_y:
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: wavesimpro
3
+ Version: 1.9.0
4
+ Summary: WavesimPro Python API — cloud execution client for the Rayfos simulation platform
5
+ Author: Rayfos
6
+ Project-URL: Repository, https://github.com/rayfos/wavesimpro
7
+ Keywords: wavesim,simulation,electromagnetic,rayfos,api
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: numpy>=1.20.0
16
+ Requires-Dist: requests>=2.28.0
17
+ Requires-Dist: python-dotenv>=1.0.0
18
+ Requires-Dist: tqdm>=4.60.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
21
+ Requires-Dist: black>=23.0.0; extra == "dev"
22
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
23
+
24
+ # wavesimpro
25
+
26
+ Python client for the Rayfos Wavesim cloud: build a simulation setup, run it on a Rayfos
27
+ compute node, and read the results back as NumPy arrays.
28
+
29
+ Wavesim is a single-wavelength frequency-domain solver (vectorial Maxwell when a polarized
30
+ source is present, scalar Helmholtz otherwise). This package talks to the Rayfos JobServer
31
+ REST API; it does not contain the solver.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install wavesimpro
37
+ python -m wavesimpro setup # one-time: API key + server URL wizard
38
+ ```
39
+
40
+ The wizard stores the credentials; `wavesimpro.configure(api_key=..., api_url=...)` sets
41
+ them for one process instead.
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ import wavesimpro as wp
47
+
48
+ props = wp.create_wavesim_properties(
49
+ wavelength=1.0, pixel_size=0.1, boundary_width=5.0,
50
+ domain=wp.create_simulation_domain(20, 20, 20),
51
+ mediums=[wp.create_medium("Silicon", 3.48, 0.0)],
52
+ primitives=[wp.create_box_primitive("core", 0, 0, 0, 10, 0.5, 20, medium="Silicon")],
53
+ sources=[wp.create_mode_source("in", 0, 0, -8.0, polarization="X")],
54
+ monitors=[wp.create_monitor("out", 0, 0, 8.0, 10, 10, 0)],
55
+ )
56
+ run = wp.simulate(props, name="strip waveguide")
57
+ field = wp.read_binary_field(run.download("output.bin"))
58
+ ```
59
+
60
+ ## What the API covers
61
+
62
+ - Setup helpers for domains, media, primitives (box, STL, voxel arrays), sources
63
+ (point, plane wave, Gaussian beam, custom field, engine-solved waveguide mode) and monitors.
64
+ - Per-axis pixel sizes (`pixel_size=[dx, dy, dz]`) and sub-pixel permittivity averaging
65
+ (`subpixel_factor=`), when the compute node supports them.
66
+ - Preflight memory check and cost estimate before a run.
67
+ - Binary upload/download helpers for permittivity volumes, source fields and monitors.
68
+
69
+ See `CHANGELOG.md` for what each version added. Issues and feature requests: support@rayfos.com.
@@ -1,3 +1,4 @@
1
+ README.md
1
2
  pyproject.toml
2
3
  src/wavesimpro/__init__.py
3
4
  src/wavesimpro/__main__.py
wavesimpro-1.6.5/PKG-INFO DELETED
@@ -1,22 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: wavesimpro
3
- Version: 1.6.5
4
- Summary: WavesimPro Python API — cloud execution client for the Rayfos simulation platform
5
- Author: Rayfos
6
- Project-URL: Repository, https://github.com/rayfos/wavesimpro
7
- Keywords: wavesim,simulation,electromagnetic,rayfos,api
8
- Classifier: Development Status :: 4 - Beta
9
- Classifier: Intended Audience :: Developers
10
- Classifier: Intended Audience :: Science/Research
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Topic :: Scientific/Engineering
13
- Requires-Python: >=3.8
14
- Description-Content-Type: text/markdown
15
- Requires-Dist: numpy>=1.20.0
16
- Requires-Dist: requests>=2.28.0
17
- Requires-Dist: python-dotenv>=1.0.0
18
- Requires-Dist: tqdm>=4.60.0
19
- Provides-Extra: dev
20
- Requires-Dist: pytest>=7.0.0; extra == "dev"
21
- Requires-Dist: black>=23.0.0; extra == "dev"
22
- Requires-Dist: mypy>=1.0.0; extra == "dev"
@@ -1,22 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: wavesimpro
3
- Version: 1.6.5
4
- Summary: WavesimPro Python API — cloud execution client for the Rayfos simulation platform
5
- Author: Rayfos
6
- Project-URL: Repository, https://github.com/rayfos/wavesimpro
7
- Keywords: wavesim,simulation,electromagnetic,rayfos,api
8
- Classifier: Development Status :: 4 - Beta
9
- Classifier: Intended Audience :: Developers
10
- Classifier: Intended Audience :: Science/Research
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Topic :: Scientific/Engineering
13
- Requires-Python: >=3.8
14
- Description-Content-Type: text/markdown
15
- Requires-Dist: numpy>=1.20.0
16
- Requires-Dist: requests>=2.28.0
17
- Requires-Dist: python-dotenv>=1.0.0
18
- Requires-Dist: tqdm>=4.60.0
19
- Provides-Extra: dev
20
- Requires-Dist: pytest>=7.0.0; extra == "dev"
21
- Requires-Dist: black>=23.0.0; extra == "dev"
22
- Requires-Dist: mypy>=1.0.0; extra == "dev"
File without changes