wavesimpro 1.10.2__tar.gz → 1.17.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 (29) hide show
  1. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/PKG-INFO +37 -2
  2. wavesimpro-1.17.0/README.md +89 -0
  3. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/pyproject.toml +1 -1
  4. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/__init__.py +2 -1
  5. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/client.py +111 -3
  6. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/execute.py +866 -848
  7. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/json_helper.py +1469 -1336
  8. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/runs.py +63 -7
  9. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/simulate.py +21 -0
  10. wavesimpro-1.17.0/src/wavesimpro/studio_mode.py +161 -0
  11. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/validate.py +2 -1
  12. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/PKG-INFO +37 -2
  13. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/SOURCES.txt +5 -1
  14. wavesimpro-1.17.0/tests/test_boundary_profile.py +33 -0
  15. wavesimpro-1.17.0/tests/test_engine_mode.py +64 -0
  16. wavesimpro-1.17.0/tests/test_studio_mode.py +100 -0
  17. wavesimpro-1.10.2/README.md +0 -54
  18. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/setup.cfg +0 -0
  19. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/__main__.py +0 -0
  20. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/binary_utils.py +0 -0
  21. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/config.py +0 -0
  22. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/exceptions.py +0 -0
  23. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/model.py +0 -0
  24. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/py.typed +0 -0
  25. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/setup.py +0 -0
  26. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
  27. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/requires.txt +0 -0
  28. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/top_level.txt +0 -0
  29. {wavesimpro-1.10.2 → wavesimpro-1.17.0}/tests/test_command_wait.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavesimpro
3
- Version: 1.10.2
3
+ Version: 1.17.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
@@ -50,7 +50,8 @@ props = wp.create_wavesim_properties(
50
50
  domain=wp.create_simulation_domain(20, 20, 20),
51
51
  mediums=[wp.create_medium("Silicon", 3.48, 0.0)],
52
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")],
53
+ # TE0 solved by the engine on the z = -6 um voxel layer (index 40 of 200), launched along +z
54
+ sources=[wp.create_mode_source("in", 0, 0, -6.0, axis="z", plane_index=40, polarization="TE")],
54
55
  monitors=[wp.create_monitor("out", 0, 0, 8.0, 10, 10, 0)],
55
56
  )
56
57
  run = wp.simulate(props, name="strip waveguide")
@@ -74,4 +75,38 @@ field = wp.read_binary_field(run.download("output.bin"))
74
75
  - Preflight memory check and cost estimate before a run.
75
76
  - Binary upload/download helpers for permittivity volumes, source fields and monitors.
76
77
 
78
+ ## Monitors: which voxels you get
79
+
80
+ A monitor is an axis-aligned box in micrometres, relative to the coordinate-system centre. The
81
+ domain of N voxels spans [-S/2, S/2], so voxel *i* occupies [-S/2 + i·v, -S/2 + (i+1)·v] with
82
+ v = S/N (centre at -S/2 + (i+0.5)·v, for even and odd N alike). The compute node extracts the
83
+ **outer cover** of the box: every voxel it touches — first index floor(lo), last index
84
+ ceil(hi) − 1 with lo/hi the box edges in voxel units. An edge exactly on a voxel face does not
85
+ pull in the neighbour; a zero-thickness box is the one voxel containing the plane. So a box of
86
+ exactly k voxels aligned to the grid gives k planes, and the same box centred on a voxel centre
87
+ straddles k + 1; the result is never smaller than what you asked for unless the domain clamps it.
88
+
89
+ The node writes `monitors.json` next to the `monitor_<name>.bin` files with, per monitor, the
90
+ inclusive index ranges, the shape, the voxel size and the realised physical box; read it with
91
+ `run.monitor_regions()`. Monitor files are full resolution — nothing is downsampled on download.
92
+
93
+ ## Cost before upload
94
+
95
+ `client.preflight_check(..., max_iterations=n)` (called for you by `simulate*`, `submit` and
96
+ `execute_via_api`) returns a `cost` block on the cloud server: the upper-bound compute-token
97
+ cost at the run's iteration cap (voxels × iterations × field components × rate), your remaining
98
+ balance and whether the submit would be allowed. The run is charged for the iterations it
99
+ actually takes. The high-level calls stop before uploading anything when the cost is refused.
100
+ `client.get_quota()` returns the balance; `client.estimate_cost(project_id, version_id)` prices a
101
+ saved version.
102
+
103
+ ## Errors and warnings
104
+
105
+ Every `RayfosAPIError` message carries the server's full detail: the message, each
106
+ `errors[]`/`warnings[]` entry and, for a quota refusal, the estimated cost and remaining balance.
107
+ Pass `sampling_warnings=False` (or `configure(sampling_warnings=False)`) to silence the
108
+ "pixels per wavelength" advisory when your sampling is deliberate; Nyquist warnings stay.
109
+ `run.status()["commandStatus"]["engine"]` names the engine the node actually ran (`"Cpp"` or
110
+ `"Python"`), also for `engine="Any"`.
111
+
77
112
  See `CHANGELOG.md` for what each version added. Issues and feature requests: support@rayfos.com.
@@ -0,0 +1,89 @@
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
+ # TE0 solved by the engine on the z = -6 um voxel layer (index 40 of 200), launched along +z
31
+ sources=[wp.create_mode_source("in", 0, 0, -6.0, axis="z", plane_index=40, polarization="TE")],
32
+ monitors=[wp.create_monitor("out", 0, 0, 8.0, 10, 10, 0)],
33
+ )
34
+ run = wp.simulate(props, name="strip waveguide")
35
+ field = wp.read_binary_field(run.download("output.bin"))
36
+ ```
37
+
38
+ ## What the API covers
39
+
40
+ - Setup helpers for domains, media, primitives (box, sphere, ellipsoid, cylinder, cone,
41
+ N-sided prism, STL, GDSII layouts, voxel arrays), sources (point, plane wave, Gaussian beam,
42
+ custom field, custom-from-file, engine-solved waveguide mode) and monitors:
43
+ `create_box_primitive`, `create_sphere_primitive`, `create_ellipsoid_primitive`,
44
+ `create_cylinder_primitive`, `create_cone_primitive`, `create_prism_primitive`,
45
+ `create_stl_primitive`, `create_gds_primitive` (with `create_gds_layer_config` /
46
+ `create_gds_structure_override`), `create_voxels_model_primitive`.
47
+ - Per-primitive sub-pixel flag (`subpixel=True/False` on every shape, STL and GDSII helper)
48
+ to choose which shapes get smoothed interfaces. Off by default: a setup with no averaged
49
+ shape is voxelised exactly as before 1.10 and runs on every compute node.
50
+ - Per-axis pixel sizes (`pixel_size=[dx, dy, dz]`) and sub-pixel permittivity averaging
51
+ (`subpixel_factor=`), when the compute node supports them.
52
+ - Preflight memory check and cost estimate before a run.
53
+ - Binary upload/download helpers for permittivity volumes, source fields and monitors.
54
+
55
+ ## Monitors: which voxels you get
56
+
57
+ A monitor is an axis-aligned box in micrometres, relative to the coordinate-system centre. The
58
+ domain of N voxels spans [-S/2, S/2], so voxel *i* occupies [-S/2 + i·v, -S/2 + (i+1)·v] with
59
+ v = S/N (centre at -S/2 + (i+0.5)·v, for even and odd N alike). The compute node extracts the
60
+ **outer cover** of the box: every voxel it touches — first index floor(lo), last index
61
+ ceil(hi) − 1 with lo/hi the box edges in voxel units. An edge exactly on a voxel face does not
62
+ pull in the neighbour; a zero-thickness box is the one voxel containing the plane. So a box of
63
+ exactly k voxels aligned to the grid gives k planes, and the same box centred on a voxel centre
64
+ straddles k + 1; the result is never smaller than what you asked for unless the domain clamps it.
65
+
66
+ The node writes `monitors.json` next to the `monitor_<name>.bin` files with, per monitor, the
67
+ inclusive index ranges, the shape, the voxel size and the realised physical box; read it with
68
+ `run.monitor_regions()`. Monitor files are full resolution — nothing is downsampled on download.
69
+
70
+ ## Cost before upload
71
+
72
+ `client.preflight_check(..., max_iterations=n)` (called for you by `simulate*`, `submit` and
73
+ `execute_via_api`) returns a `cost` block on the cloud server: the upper-bound compute-token
74
+ cost at the run's iteration cap (voxels × iterations × field components × rate), your remaining
75
+ balance and whether the submit would be allowed. The run is charged for the iterations it
76
+ actually takes. The high-level calls stop before uploading anything when the cost is refused.
77
+ `client.get_quota()` returns the balance; `client.estimate_cost(project_id, version_id)` prices a
78
+ saved version.
79
+
80
+ ## Errors and warnings
81
+
82
+ Every `RayfosAPIError` message carries the server's full detail: the message, each
83
+ `errors[]`/`warnings[]` entry and, for a quota refusal, the estimated cost and remaining balance.
84
+ Pass `sampling_warnings=False` (or `configure(sampling_warnings=False)`) to silence the
85
+ "pixels per wavelength" advisory when your sampling is deliberate; Nyquist warnings stay.
86
+ `run.status()["commandStatus"]["engine"]` names the engine the node actually ran (`"Cpp"` or
87
+ `"Python"`), also for `engine="Any"`.
88
+
89
+ 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.10.2"
7
+ version = "1.17.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"
@@ -29,6 +29,7 @@ from .binary_utils import (
29
29
  from .validate import validate_parameters, ParameterValidationError
30
30
  from .execute import execute_via_api
31
31
  from .simulate import simulate, simulate_pro, configure
32
+ from .studio_mode import solve_studio_mode, add_studio_mode_source, mode_slice
32
33
  from .json_helper import (
33
34
  create_simulation_domain,
34
35
  create_stl_primitive,
@@ -54,7 +55,7 @@ from .json_helper import (
54
55
  parse_progress_data,
55
56
  )
56
57
 
57
- __version__ = "1.10.2"
58
+ __version__ = "1.17.0"
58
59
  __all__ = [
59
60
  # Client
60
61
  "RayfosClient",
@@ -248,10 +248,11 @@ class RayfosClient:
248
248
  except requests.exceptions.HTTPError as e:
249
249
  status_code = response.status_code
250
250
 
251
- # Try to get error message from response
251
+ # The server's full error detail: message + errors[].details + the quota figures of
252
+ # a 402. It used to be only "message"; the useful part sat in exception.response.text.
252
253
  try:
253
254
  error_data = response.json()
254
- message = error_data.get("message") or error_data.get("error") or str(e)
255
+ message = format_error_body(error_data, str(e))
255
256
  except ValueError:
256
257
  message = response.text or str(e)
257
258
 
@@ -329,10 +330,11 @@ class RayfosClient:
329
330
  engine_preference: str = "Any",
330
331
  use_gpu: Optional[bool] = None,
331
332
  is_vectorial: Optional[bool] = None,
333
+ max_iterations: Optional[int] = None,
332
334
  ) -> Dict[str, Any]:
333
335
  """
334
336
  Ask the server whether a simulation of the given shape will fit on the
335
- available compute targets, BEFORE uploading any files.
337
+ available compute targets, and what it will cost, BEFORE uploading any files.
336
338
 
337
339
  This calls POST /api/preflightCheck. The server runs the engine's own
338
340
  memory predictor (Wavesim.MemoryPredict) on exactly the grid the engine
@@ -352,16 +354,31 @@ class RayfosClient:
352
354
  is_vectorial: True = Maxwell (3 field components), False = Helmholtz
353
355
  (scalar). Memory differs ~3x, so always pass it: this payload
354
356
  carries no sources for the server to infer it from.
357
+ max_iterations: the run's residual-norm iteration cap. The cost is
358
+ priced on it (voxels x iterations x components x rate), so pass
359
+ the value the run will use; None = the server default (10000).
355
360
 
356
361
  Returns:
357
362
  PreflightCheckResponse dict with keys:
358
363
  - status: "Ok" | "Warning" | "Error"
359
364
  - description: one-sentence verdict (memory, grid, which machine fits)
360
365
  - gpuPathMemoryGB / cpuPathMemoryGB: peak memory of each path
366
+ - hostMemoryGB: peak system RAM on the compute node for the requested
367
+ path (voxelization, or the result field gathered back to the host);
368
+ checked against each node's RAM since server 1.17.0
361
369
  - estimate: { nx, ny, nz, paddedNx, ..., totalMemoryGB, ... } for the requested path
362
370
  - machines: [{ machineId, gpuMemoryGB, cpuMemoryGB, fitsOnGpu, fitsOnCpu, ... }]
363
371
  - recommendedMachineId
364
372
  - suggestedAdjustments: [str, ...]
373
+ - cost (cloud server only, else absent/None): the compute-token
374
+ check the submit will apply:
375
+ { allowed, reason, enforced, computeTokensRemaining,
376
+ estimate: { estimatedCost, iterations, totalVoxels,
377
+ componentsPerCell, rate, target, ... } }
378
+ estimatedCost is the UPPER BOUND (max_iterations); the run is
379
+ reconciled to the iterations it actually took. Stop before
380
+ uploading when cost["allowed"] is False. See
381
+ ``preflight_cost_summary`` for a one-line rendering.
365
382
  """
366
383
  size_x, size_y, size_z = domain_um
367
384
  periodic_x, periodic_y, periodic_z = periodic
@@ -399,6 +416,7 @@ class RayfosClient:
399
416
  "periodicX": bool(periodic_x),
400
417
  "periodicY": bool(periodic_y),
401
418
  "periodicZ": bool(periodic_z),
419
+ **({"residualNormMaxIterations": int(max_iterations)} if max_iterations else {}),
402
420
  },
403
421
  "enginePreference": engine_preference,
404
422
  "useGpu": use_gpu,
@@ -415,6 +433,38 @@ class RayfosClient:
415
433
  response = self.session.post(url, json=body, timeout=self.config.timeout, verify=verify)
416
434
  return self._handle_response(response)
417
435
 
436
+ def get_quota(self) -> Dict[str, Any]:
437
+ """
438
+ The caller's compute-token and storage quota: GET /api/quota/me.
439
+
440
+ Returns a dict with (camelCase) ``computeTokensGranted``, ``computeTokensUsed``,
441
+ ``computeTokensReserved``, ``computeTokensRemaining``, the storage figures and the
442
+ allowance period. ``computeTokensRemaining`` is what a submit is checked against.
443
+ """
444
+ return self._get("/api/quota/me")
445
+
446
+ def estimate_cost(
447
+ self,
448
+ project_id: str,
449
+ version_id: str,
450
+ use_gpu: Optional[bool] = None,
451
+ engine_preference: str = "Any",
452
+ ) -> Dict[str, Any]:
453
+ """
454
+ Price a SAVED version without submitting it: POST /api/quota/estimate.
455
+
456
+ Same result shape as the ``cost`` block of ``preflight_check``: ``allowed``,
457
+ ``reason``, ``computeTokensRemaining`` and ``estimate.estimatedCost`` (the upper
458
+ bound at the version's max iterations). Use ``preflight_check(..., max_iterations=)``
459
+ instead when nothing has been uploaded yet.
460
+ """
461
+ return self._post("/api/quota/estimate", json={
462
+ "projectId": project_id,
463
+ "versionId": version_id,
464
+ "useGpu": use_gpu,
465
+ "enginePreference": engine_preference,
466
+ })
467
+
418
468
  def get_projects(self) -> List[Dict[str, Any]]:
419
469
  """
420
470
  Get all projects for the authenticated user
@@ -1890,3 +1940,61 @@ class RayfosClient:
1890
1940
  """Command state name from an integer (legacy ``commandStatus``) or a name (current ``status``)."""
1891
1941
  return command_status_name({"status": status_value})
1892
1942
 
1943
+
1944
+ def format_error_body(data: Any, fallback: str) -> str:
1945
+ """Render a Rayfos error body in full: ``message``, then each of ``errors[]`` /
1946
+ ``warnings[]`` (strings, or ``{code, message, field, details}`` objects from the server's
1947
+ simulation validation), then the figures of a quota refusal (402: ``estimatedCost``,
1948
+ ``remaining``, ``resolution``, ``maxGridCells``)."""
1949
+ if not isinstance(data, dict):
1950
+ return str(data) if data else fallback
1951
+ parts = []
1952
+ head = data.get("message") or data.get("error") or data.get("title") or data.get("detail")
1953
+ parts.append(str(head) if head else fallback)
1954
+ for key, label in (("errors", "error"), ("warnings", "warning")):
1955
+ items = data.get(key)
1956
+ if not isinstance(items, list):
1957
+ continue
1958
+ for it in items:
1959
+ if isinstance(it, dict):
1960
+ bits = [str(it[k]) for k in ("field", "code", "message", "details", "detail") if it.get(k)]
1961
+ text = ": ".join(bits) if bits else str(it)
1962
+ else:
1963
+ text = str(it)
1964
+ parts.append(f" [{label}] {text}")
1965
+ quota = [f"{k}={data[k]}" for k in ("reason", "estimatedCost", "remaining", "resolution", "totalVoxels", "maxGridCells")
1966
+ if data.get(k) is not None]
1967
+ if quota:
1968
+ parts.append(" " + ", ".join(quota))
1969
+ return "\n".join(parts)
1970
+
1971
+
1972
+ def preflight_cost_summary(preflight: Optional[Dict[str, Any]]) -> Optional[str]:
1973
+ """One line describing the ``cost`` block of a ``preflight_check`` result, or None when the
1974
+ server did not price the run (local machine service / older server)."""
1975
+ cost = (preflight or {}).get("cost") if isinstance(preflight, dict) else None
1976
+ if not isinstance(cost, dict):
1977
+ return None
1978
+ est = cost.get("estimate") or {}
1979
+ tokens = est.get("estimatedCost")
1980
+ remaining = cost.get("computeTokensRemaining")
1981
+ iters = est.get("iterations")
1982
+ voxels = est.get("totalVoxels")
1983
+ comps = est.get("componentsPerCell")
1984
+ text = f"Cost: up to {tokens:,} compute tokens" if isinstance(tokens, (int, float)) else "Cost: unknown"
1985
+ detail = []
1986
+ if voxels:
1987
+ detail.append(f"{voxels:,} voxels")
1988
+ if iters:
1989
+ detail.append(f"max {iters:,} iterations")
1990
+ if comps:
1991
+ detail.append(f"{comps} component(s)")
1992
+ if detail:
1993
+ text += " (" + " x ".join(detail) + "; charged for the iterations actually run)"
1994
+ if isinstance(remaining, (int, float)):
1995
+ text += f"; balance {remaining:,}"
1996
+ if cost.get("enforced") is False:
1997
+ text += "; quota not enforced on this server"
1998
+ elif cost.get("allowed") is False:
1999
+ text += f"; NOT allowed ({cost.get('reason')})"
2000
+ return text