wavesimpro 1.6.5__tar.gz → 1.10.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 (24) hide show
  1. wavesimpro-1.10.0/PKG-INFO +76 -0
  2. wavesimpro-1.10.0/README.md +53 -0
  3. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/pyproject.toml +1 -1
  4. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/__init__.py +19 -1
  5. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/client.py +12 -2
  6. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/execute.py +51 -18
  7. wavesimpro-1.10.0/src/wavesimpro/json_helper.py +1336 -0
  8. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/simulate.py +11 -7
  9. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/validate.py +46 -21
  10. wavesimpro-1.10.0/src/wavesimpro.egg-info/PKG-INFO +76 -0
  11. {wavesimpro-1.6.5 → wavesimpro-1.10.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/json_helper.py +0 -738
  14. wavesimpro-1.6.5/src/wavesimpro.egg-info/PKG-INFO +0 -22
  15. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/setup.cfg +0 -0
  16. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/__main__.py +0 -0
  17. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/binary_utils.py +0 -0
  18. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/config.py +0 -0
  19. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/exceptions.py +0 -0
  20. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/py.typed +0 -0
  21. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro/setup.py +0 -0
  22. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
  23. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro.egg-info/requires.txt +0 -0
  24. {wavesimpro-1.6.5 → wavesimpro-1.10.0}/src/wavesimpro.egg-info/top_level.txt +0 -0
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: wavesimpro
3
+ Version: 1.10.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, sphere, ellipsoid, cylinder, cone,
63
+ N-sided prism, STL, GDSII layouts, voxel arrays), sources (point, plane wave, Gaussian beam,
64
+ custom field, custom-from-file, engine-solved waveguide mode) and monitors:
65
+ `create_box_primitive`, `create_sphere_primitive`, `create_ellipsoid_primitive`,
66
+ `create_cylinder_primitive`, `create_cone_primitive`, `create_prism_primitive`,
67
+ `create_stl_primitive`, `create_gds_primitive` (with `create_gds_layer_config` /
68
+ `create_gds_structure_override`), `create_voxels_model_primitive`.
69
+ - Per-primitive sub-pixel flag (`subpixel=True/False` on every shape, STL and GDSII helper)
70
+ to choose which shapes get smoothed interfaces.
71
+ - Per-axis pixel sizes (`pixel_size=[dx, dy, dz]`) and sub-pixel permittivity averaging
72
+ (`subpixel_factor=`), when the compute node supports them.
73
+ - Preflight memory check and cost estimate before a run.
74
+ - Binary upload/download helpers for permittivity volumes, source fields and monitors.
75
+
76
+ See `CHANGELOG.md` for what each version added. Issues and feature requests: support@rayfos.com.
@@ -0,0 +1,53 @@
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, sphere, ellipsoid, cylinder, cone,
40
+ N-sided prism, STL, GDSII layouts, voxel arrays), sources (point, plane wave, Gaussian beam,
41
+ custom field, custom-from-file, engine-solved waveguide mode) and monitors:
42
+ `create_box_primitive`, `create_sphere_primitive`, `create_ellipsoid_primitive`,
43
+ `create_cylinder_primitive`, `create_cone_primitive`, `create_prism_primitive`,
44
+ `create_stl_primitive`, `create_gds_primitive` (with `create_gds_layer_config` /
45
+ `create_gds_structure_override`), `create_voxels_model_primitive`.
46
+ - Per-primitive sub-pixel flag (`subpixel=True/False` on every shape, STL and GDSII helper)
47
+ to choose which shapes get smoothed interfaces.
48
+ - Per-axis pixel sizes (`pixel_size=[dx, dy, dz]`) and sub-pixel permittivity averaging
49
+ (`subpixel_factor=`), when the compute node supports them.
50
+ - Preflight memory check and cost estimate before a run.
51
+ - Binary upload/download helpers for permittivity volumes, source fields and monitors.
52
+
53
+ 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.10.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"
@@ -33,10 +33,19 @@ from .json_helper import (
33
33
  create_simulation_domain,
34
34
  create_stl_primitive,
35
35
  create_box_primitive,
36
+ create_sphere_primitive,
37
+ create_ellipsoid_primitive,
38
+ create_cylinder_primitive,
39
+ create_cone_primitive,
40
+ create_prism_primitive,
41
+ create_gds_primitive,
42
+ create_gds_layer_config,
43
+ create_gds_structure_override,
36
44
  create_medium,
37
45
  create_point_source,
38
46
  create_plane_wave_source,
39
47
  create_gaussian_beam_source,
48
+ create_mode_source,
40
49
  create_voxels_model_primitive,
41
50
  create_custom_field_source,
42
51
  create_custom_from_file_source,
@@ -45,7 +54,7 @@ from .json_helper import (
45
54
  parse_progress_data,
46
55
  )
47
56
 
48
- __version__ = "1.6.5"
57
+ __version__ = "1.10.0"
49
58
  __all__ = [
50
59
  # Client
51
60
  "RayfosClient",
@@ -77,10 +86,19 @@ __all__ = [
77
86
  "create_simulation_domain",
78
87
  "create_stl_primitive",
79
88
  "create_box_primitive",
89
+ "create_sphere_primitive",
90
+ "create_ellipsoid_primitive",
91
+ "create_cylinder_primitive",
92
+ "create_cone_primitive",
93
+ "create_prism_primitive",
94
+ "create_gds_primitive",
95
+ "create_gds_layer_config",
96
+ "create_gds_structure_override",
80
97
  "create_medium",
81
98
  "create_point_source",
82
99
  "create_plane_wave_source",
83
100
  "create_gaussian_beam_source",
101
+ "create_mode_source",
84
102
  "create_voxels_model_primitive",
85
103
  "create_custom_field_source",
86
104
  "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}")