wavesimpro 1.16.0__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.
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/PKG-INFO +35 -1
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/README.md +34 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/pyproject.toml +1 -1
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/__init__.py +1 -1
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/client.py +111 -3
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/execute.py +10 -1
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/json_helper.py +28 -6
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/runs.py +35 -7
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/simulate.py +17 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/validate.py +2 -1
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/PKG-INFO +35 -1
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/setup.cfg +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/__main__.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/binary_utils.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/config.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/exceptions.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/model.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/py.typed +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/setup.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro/studio_mode.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/SOURCES.txt +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/requires.txt +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/top_level.txt +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/tests/test_boundary_profile.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/tests/test_command_wait.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/tests/test_engine_mode.py +0 -0
- {wavesimpro-1.16.0 → wavesimpro-1.17.0}/tests/test_studio_mode.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavesimpro
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -75,4 +75,38 @@ field = wp.read_binary_field(run.download("output.bin"))
|
|
|
75
75
|
- Preflight memory check and cost estimate before a run.
|
|
76
76
|
- Binary upload/download helpers for permittivity volumes, source fields and monitors.
|
|
77
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
|
+
|
|
78
112
|
See `CHANGELOG.md` for what each version added. Issues and feature requests: support@rayfos.com.
|
|
@@ -52,4 +52,38 @@ field = wp.read_binary_field(run.download("output.bin"))
|
|
|
52
52
|
- Preflight memory check and cost estimate before a run.
|
|
53
53
|
- Binary upload/download helpers for permittivity volumes, source fields and monitors.
|
|
54
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
|
+
|
|
55
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.
|
|
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"
|
|
@@ -248,10 +248,11 @@ class RayfosClient:
|
|
|
248
248
|
except requests.exceptions.HTTPError as e:
|
|
249
249
|
status_code = response.status_code
|
|
250
250
|
|
|
251
|
-
#
|
|
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 =
|
|
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
|
|
@@ -19,7 +19,7 @@ from typing import Tuple, Optional, Callable, Dict, Any, List, Union, Sequence
|
|
|
19
19
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
|
|
22
|
-
from .client import RayfosClient
|
|
22
|
+
from .client import RayfosClient, preflight_cost_summary
|
|
23
23
|
from .binary_utils import (
|
|
24
24
|
upload_array_as_binary_zipped,
|
|
25
25
|
upload_source_field_as_binary_zipped,
|
|
@@ -147,6 +147,7 @@ def execute_via_api(
|
|
|
147
147
|
alpha: float = 0.75,
|
|
148
148
|
keep_residuals_list: bool = False,
|
|
149
149
|
strict_validation: bool = False,
|
|
150
|
+
sampling_warnings: bool = True,
|
|
150
151
|
gpu_memory_max_gb: Optional[float] = None,
|
|
151
152
|
monitors: Optional[List[Dict[str, Any]]] = None,
|
|
152
153
|
pseudo_propagation_distance: float = 0.0,
|
|
@@ -374,6 +375,7 @@ def execute_via_api(
|
|
|
374
375
|
engine_preference=engine_preference,
|
|
375
376
|
use_gpu=use_gpu,
|
|
376
377
|
is_vectorial=is_vectorial,
|
|
378
|
+
max_iterations=max_iterations,
|
|
377
379
|
)
|
|
378
380
|
except Exception as exc:
|
|
379
381
|
logger.warning(f"Preflight check failed ({exc}). Proceeding without it.")
|
|
@@ -393,6 +395,12 @@ def execute_via_api(
|
|
|
393
395
|
(logger.warning if status == "warning" else logger.info)(f"Preflight: {pre_description}")
|
|
394
396
|
for s in suggestions:
|
|
395
397
|
logger.info(f" Suggestion: {s}")
|
|
398
|
+
cost_line = preflight_cost_summary(pre)
|
|
399
|
+
if cost_line:
|
|
400
|
+
logger.info(f"Preflight: {cost_line}")
|
|
401
|
+
cost = pre.get("cost") or {}
|
|
402
|
+
if cost.get("allowed") is False and cost.get("enforced", True):
|
|
403
|
+
raise RuntimeError(f"Preflight refused the simulation before upload: {cost_line}")
|
|
396
404
|
|
|
397
405
|
# 6. Create or reuse project/version
|
|
398
406
|
auto_create = not project_id or not version_id
|
|
@@ -422,6 +430,7 @@ def execute_via_api(
|
|
|
422
430
|
em_data, sources_info, wavelength, ps,
|
|
423
431
|
threshold, max_iterations, boundary_width, is_vectorial,
|
|
424
432
|
strict_validation=strict_validation,
|
|
433
|
+
sampling_warnings=sampling_warnings,
|
|
425
434
|
gpu_memory_max_gb=gpu_memory_max_gb,
|
|
426
435
|
periodic=periodic,
|
|
427
436
|
pseudo_propagation_distance=pseudo_propagation_distance,
|
|
@@ -1160,16 +1160,34 @@ def create_monitor(
|
|
|
1160
1160
|
Create a monitor: a positioned 3D subcube (region of interest) of the field.
|
|
1161
1161
|
|
|
1162
1162
|
The monitor result is written in the same binary format as output.bin
|
|
1163
|
-
(complex64, all polarization components)
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
(large monitors are decimated at fetch time, like output.bin).
|
|
1163
|
+
(complex64, all polarization components) at FULL resolution — nothing is
|
|
1164
|
+
downsampled on the way to you — so it is read with read_monitor_binary /
|
|
1165
|
+
read_binary_field. Matches RayfosCommandModels.WavesimMonitor.
|
|
1167
1166
|
|
|
1168
1167
|
Two positioning modes:
|
|
1169
1168
|
- CenterAndSize (default): ``center_*`` is the cuboid centre, ``size_*`` its
|
|
1170
1169
|
Width/Height/Depth (X/Y/Z extents).
|
|
1171
1170
|
- MinMaxBounds: pass ``min_bounds`` and ``max_bounds`` as (x, y, z) corners; the
|
|
1172
|
-
center/size arguments are then ignored
|
|
1171
|
+
center/size arguments are then ignored (Position/Size are filled in from the
|
|
1172
|
+
corners so every reader, Studio included, sees the same box).
|
|
1173
|
+
|
|
1174
|
+
Coordinates are relative to the coordinate-system centre of the setup (0 unless
|
|
1175
|
+
you moved it); the domain itself is centred there.
|
|
1176
|
+
|
|
1177
|
+
Which voxels a monitor selects (the same rule on every axis): the domain of N
|
|
1178
|
+
voxels spans [-S/2, S/2], so voxel i occupies [-S/2 + i*v, -S/2 + (i+1)*v] with
|
|
1179
|
+
v = S/N (its centre is at -S/2 + (i+0.5)*v, for even and odd N alike). The
|
|
1180
|
+
monitor takes the OUTER COVER of its box: every voxel the box touches, first
|
|
1181
|
+
index floor(lo), last index ceil(hi) - 1 with lo/hi the box edges in voxel
|
|
1182
|
+
units; an edge exactly on a voxel face does not pull in the neighbour, and a
|
|
1183
|
+
zero-thickness box selects the one voxel containing the plane. So a box of
|
|
1184
|
+
exactly k voxels aligned to the grid returns k planes, while the same box
|
|
1185
|
+
centred on a voxel centre straddles k+1. The realised region is never smaller
|
|
1186
|
+
than what you asked for (unless clamped by the domain). The compute node writes
|
|
1187
|
+
the exact result per monitor to ``monitors.json`` next to the monitor files:
|
|
1188
|
+
inclusive voxel index ranges, the shape, the voxel size and the realised
|
|
1189
|
+
physical box (``Run.monitor_regions()`` reads it). Work in multiples of the
|
|
1190
|
+
voxel size, aligned to voxel faces, when you need an exact plane count.
|
|
1173
1191
|
|
|
1174
1192
|
Args:
|
|
1175
1193
|
name: Monitor name.
|
|
@@ -1186,6 +1204,11 @@ def create_monitor(
|
|
|
1186
1204
|
return {"X": _len(t[0]), "Y": _len(t[1]), "Z": _len(t[2])}
|
|
1187
1205
|
|
|
1188
1206
|
use_bounds = min_bounds is not None and max_bounds is not None
|
|
1207
|
+
if use_bounds:
|
|
1208
|
+
lo = [min(float(a), float(b)) for a, b in zip(min_bounds, max_bounds)]
|
|
1209
|
+
hi = [max(float(a), float(b)) for a, b in zip(min_bounds, max_bounds)]
|
|
1210
|
+
center_x, center_y, center_z = [(l + h) / 2 for l, h in zip(lo, hi)]
|
|
1211
|
+
size_x, size_y, size_z = [h - l for l, h in zip(lo, hi)]
|
|
1189
1212
|
return {
|
|
1190
1213
|
"Id": str(uuid.uuid4()),
|
|
1191
1214
|
"Name": name,
|
|
@@ -1205,7 +1228,6 @@ def create_monitor(
|
|
|
1205
1228
|
"MaxBounds": _xyz(max_bounds) if use_bounds else None,
|
|
1206
1229
|
}
|
|
1207
1230
|
|
|
1208
|
-
|
|
1209
1231
|
def create_wavesim_properties(
|
|
1210
1232
|
simulation_domain: Dict[str, Any],
|
|
1211
1233
|
primitives: List[Dict[str, Any]],
|
|
@@ -34,7 +34,7 @@ from .binary_utils import (
|
|
|
34
34
|
upload_array_as_binary_zipped,
|
|
35
35
|
upload_source_field_as_binary_zipped,
|
|
36
36
|
)
|
|
37
|
-
from .client import RayfosClient, command_status_name, is_terminal_command_status
|
|
37
|
+
from .client import RayfosClient, command_status_name, is_terminal_command_status, preflight_cost_summary
|
|
38
38
|
from .json_helper import (
|
|
39
39
|
create_custom_field_source,
|
|
40
40
|
create_simulation_domain,
|
|
@@ -536,8 +536,9 @@ class Setup:
|
|
|
536
536
|
subpixel_factor=self.subpixel_factor if any_smooth else None,
|
|
537
537
|
)
|
|
538
538
|
|
|
539
|
-
def validate(self, strict: bool = False) -> List[str]:
|
|
540
|
-
"""Client-side parameter validation (the server preflight decides memory).
|
|
539
|
+
def validate(self, strict: bool = False, sampling_warnings: bool = True) -> List[str]:
|
|
540
|
+
"""Client-side parameter validation (the server preflight decides memory).
|
|
541
|
+
``sampling_warnings=False`` silences the pixels-per-wavelength advisory."""
|
|
541
542
|
dx, dy, dz = self.pixel_sizes
|
|
542
543
|
sources_info = []
|
|
543
544
|
for s in self.field_sources:
|
|
@@ -551,7 +552,7 @@ class Setup:
|
|
|
551
552
|
domain.array if domain is not None else None,
|
|
552
553
|
sources_info, self.wavelength, self.pixel_sizes,
|
|
553
554
|
self.threshold, self.max_iterations, self.boundary_width, self.is_vectorial,
|
|
554
|
-
strict_validation=strict, periodic=self.periodic,
|
|
555
|
+
strict_validation=strict, sampling_warnings=sampling_warnings, periodic=self.periodic,
|
|
555
556
|
pseudo_propagation_distance=self.pseudo_propagation_distance,
|
|
556
557
|
data_type=domain.data_type if domain is not None else "Permittivity",
|
|
557
558
|
grid_shape=self.grid, n_max=self.n_max(),
|
|
@@ -574,6 +575,7 @@ def submit(
|
|
|
574
575
|
version_id: Optional[str] = None,
|
|
575
576
|
machine_service_url: Optional[str] = None,
|
|
576
577
|
strict_validation: bool = False,
|
|
578
|
+
sampling_warnings: bool = True,
|
|
577
579
|
description: Optional[str] = None,
|
|
578
580
|
client: Optional[RayfosClient] = None,
|
|
579
581
|
) -> "Run":
|
|
@@ -588,6 +590,8 @@ def submit(
|
|
|
588
590
|
project_id, version_id: reuse an existing project/version instead of creating one.
|
|
589
591
|
machine_service_url: talk to a machine service directly (local mode).
|
|
590
592
|
strict_validation: soft validation issues raise instead of warning.
|
|
593
|
+
sampling_warnings: False silences the "pixels per wavelength" advisory when the
|
|
594
|
+
sampling is deliberate (Nyquist warnings stay).
|
|
591
595
|
description: project name / job label (default: ``setup.name`` or the calling script).
|
|
592
596
|
client: a ready ``RayfosClient`` (tests / custom sessions); api_key/api_url are then unused.
|
|
593
597
|
"""
|
|
@@ -617,7 +621,7 @@ def submit(
|
|
|
617
621
|
f"Monitors: {len(setup.monitors)}")
|
|
618
622
|
|
|
619
623
|
# 1. Client-side validation and overlap check (nothing uploaded yet).
|
|
620
|
-
setup.validate(strict=strict_validation)
|
|
624
|
+
setup.validate(strict=strict_validation, sampling_warnings=sampling_warnings)
|
|
621
625
|
setup.check_overlaps()
|
|
622
626
|
props_preview = setup.to_properties() # raises early on an inconsistent setup
|
|
623
627
|
|
|
@@ -627,7 +631,7 @@ def submit(
|
|
|
627
631
|
try:
|
|
628
632
|
pre = client.preflight_check(domain_um=setup.domain_um, voxel_um=ps, boundary_sides_um=sides,
|
|
629
633
|
periodic=setup.periodic, engine_preference=engine, use_gpu=use_gpu,
|
|
630
|
-
is_vectorial=vectorial)
|
|
634
|
+
is_vectorial=vectorial, max_iterations=setup.max_iterations)
|
|
631
635
|
except Exception as exc: # the preflight is advisory when the endpoint is unreachable
|
|
632
636
|
logger.warning(f"Preflight check failed ({exc}). Proceeding without it.")
|
|
633
637
|
pre = None
|
|
@@ -641,6 +645,12 @@ def submit(
|
|
|
641
645
|
(logger.warning if status == "warning" else logger.info)(f"Preflight: {text}")
|
|
642
646
|
for s in suggestions:
|
|
643
647
|
logger.info(f" Suggestion: {s}")
|
|
648
|
+
cost_line = preflight_cost_summary(pre)
|
|
649
|
+
if cost_line:
|
|
650
|
+
logger.info(f"Preflight: {cost_line}")
|
|
651
|
+
cost = pre.get("cost") or {}
|
|
652
|
+
if cost.get("allowed") is False and cost.get("enforced", True):
|
|
653
|
+
raise RuntimeError(f"Preflight refused the simulation before upload: {cost_line}")
|
|
644
654
|
|
|
645
655
|
# 3. Project / version.
|
|
646
656
|
if not project_id or not version_id:
|
|
@@ -905,6 +915,23 @@ class Run:
|
|
|
905
915
|
data = gzip.decompress(data)
|
|
906
916
|
return json.loads(data.decode("utf-8-sig"))
|
|
907
917
|
|
|
918
|
+
def monitor_regions(self) -> Dict[str, Dict[str, Any]]:
|
|
919
|
+
"""What each monitor file actually contains, from the ``monitors.json`` the compute node
|
|
920
|
+
writes next to the ``monitor_<name>.bin`` files: ``{name: {"File", "IndexMin", "IndexMax"
|
|
921
|
+
(inclusive voxel indices, x/y/z), "Shape", "VoxelSizeUm", "DomainShape", "RequestedMinUm",
|
|
922
|
+
"RequestedMaxUm", "RealisedMinUm", "RealisedMaxUm"}}``. The realised box is the outer cover
|
|
923
|
+
of the requested one (see ``create_monitor``), in the monitor's own coordinates. Raises
|
|
924
|
+
``FileNotFoundError`` for runs from nodes older than 1.17 (no monitors.json)."""
|
|
925
|
+
f = self.find_file("monitors.json")
|
|
926
|
+
if f is None:
|
|
927
|
+
raise FileNotFoundError("the run has no monitors.json: no monitor, or the compute node predates 1.17")
|
|
928
|
+
data = self.client.download_file_data(self.project_id, self.version_id, f["id"], command_id=self.command_id)
|
|
929
|
+
if data[:2] == b"\x1f\x8b":
|
|
930
|
+
import gzip
|
|
931
|
+
data = gzip.decompress(data)
|
|
932
|
+
doc = json.loads(data.decode("utf-8-sig"))
|
|
933
|
+
return {m.get("Name", ""): m for m in doc.get("Monitors", [])}
|
|
934
|
+
|
|
908
935
|
def download(self, name: str, dest: Optional[Union[str, Path]] = None) -> Path:
|
|
909
936
|
"""Download one output file by name (e.g. ``"output.bin"``, ``"wavesim.json"``,
|
|
910
937
|
``"permittivityDomain1.bin"``). ``dest`` is a directory or a file path; default:
|
|
@@ -952,7 +979,8 @@ class Run:
|
|
|
952
979
|
return self._read_file("output.bin", dims)
|
|
953
980
|
|
|
954
981
|
def get_monitors(self) -> List[np.ndarray]:
|
|
955
|
-
"""The monitor fields, in the order the monitors were added (disabled ones skipped)
|
|
982
|
+
"""The monitor fields, in the order the monitors were added (disabled ones skipped), at
|
|
983
|
+
full resolution. ``monitor_regions()`` tells which voxels each one holds."""
|
|
956
984
|
self._ensure_done()
|
|
957
985
|
by_name = {f["name"].lower(): f for f in self.list_files()}
|
|
958
986
|
ordered = [(m.get("Name", ""), monitor_output_filename(m.get("Name", "")))
|
|
@@ -42,6 +42,7 @@ _defaults = {
|
|
|
42
42
|
"engine_preference": "Any",
|
|
43
43
|
"use_gpu": None,
|
|
44
44
|
"strict_validation": False,
|
|
45
|
+
"sampling_warnings": True,
|
|
45
46
|
"gpu_memory_max_gb": None,
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -55,6 +56,7 @@ def configure(
|
|
|
55
56
|
engine_preference: Optional[str] = None,
|
|
56
57
|
use_gpu: Optional[bool] = None,
|
|
57
58
|
strict_validation=_UNSET,
|
|
59
|
+
sampling_warnings: Optional[bool] = None,
|
|
58
60
|
gpu_memory_max_gb=_UNSET,
|
|
59
61
|
):
|
|
60
62
|
"""Set module-level defaults for remote execution.
|
|
@@ -72,6 +74,9 @@ def configure(
|
|
|
72
74
|
strict_validation: When True, soft validation issues (currently: a
|
|
73
75
|
source field larger than the domain) raise instead of warning.
|
|
74
76
|
Default False — simulations are not stopped by these.
|
|
77
|
+
sampling_warnings: False silences the advisory "Only N pixels per
|
|
78
|
+
wavelength ... recommend >= 10" when your sampling is deliberate.
|
|
79
|
+
Nyquist-limit warnings are kept. Default True.
|
|
75
80
|
gpu_memory_max_gb: DEPRECATED and ignored (1.6.2). Memory fit is
|
|
76
81
|
decided by the server preflight (/api/preflightCheck), which knows
|
|
77
82
|
the engine's real allocation model and the registered machines'
|
|
@@ -92,6 +97,8 @@ def configure(
|
|
|
92
97
|
_defaults["use_gpu"] = use_gpu
|
|
93
98
|
if strict_validation is not _UNSET:
|
|
94
99
|
_defaults["strict_validation"] = bool(strict_validation)
|
|
100
|
+
if sampling_warnings is not None:
|
|
101
|
+
_defaults["sampling_warnings"] = bool(sampling_warnings)
|
|
95
102
|
if gpu_memory_max_gb is not _UNSET:
|
|
96
103
|
_defaults["gpu_memory_max_gb"] = gpu_memory_max_gb
|
|
97
104
|
|
|
@@ -115,6 +122,7 @@ def simulate(
|
|
|
115
122
|
crop_boundaries: bool = True, # local only — accepted and ignored
|
|
116
123
|
callback: Optional[Callable] = None,
|
|
117
124
|
strict_validation: Optional[bool] = None,
|
|
125
|
+
sampling_warnings: Optional[bool] = None, # False silences the pixels-per-wavelength advisory
|
|
118
126
|
gpu_memory_max_gb=_UNSET,
|
|
119
127
|
monitors=None, # list of dicts from create_monitor(); each captures a 3D ROI of the field
|
|
120
128
|
pseudo_propagation_distance: float = 0.0, # μm; 0 = off. Overrides max_iterations. C++ engine only.
|
|
@@ -178,6 +186,9 @@ def simulate(
|
|
|
178
186
|
effective_strict = (
|
|
179
187
|
_defaults["strict_validation"] if strict_validation is None else bool(strict_validation)
|
|
180
188
|
)
|
|
189
|
+
effective_sampling = (
|
|
190
|
+
_defaults["sampling_warnings"] if sampling_warnings is None else bool(sampling_warnings)
|
|
191
|
+
)
|
|
181
192
|
effective_gpu_max = (
|
|
182
193
|
_defaults["gpu_memory_max_gb"] if gpu_memory_max_gb is _UNSET else gpu_memory_max_gb
|
|
183
194
|
)
|
|
@@ -239,6 +250,7 @@ def simulate(
|
|
|
239
250
|
keep_residuals_list=full_residuals,
|
|
240
251
|
progress_callback=_progress_cb,
|
|
241
252
|
strict_validation=effective_strict,
|
|
253
|
+
sampling_warnings=effective_sampling,
|
|
242
254
|
gpu_memory_max_gb=effective_gpu_max,
|
|
243
255
|
monitors=monitors,
|
|
244
256
|
pseudo_propagation_distance=pseudo_propagation_distance,
|
|
@@ -278,6 +290,7 @@ def simulate_pro(
|
|
|
278
290
|
description: Optional[str] = None,
|
|
279
291
|
server: Optional[str] = None,
|
|
280
292
|
strict_validation: Optional[bool] = None,
|
|
293
|
+
sampling_warnings: Optional[bool] = None, # False silences the pixels-per-wavelength advisory
|
|
281
294
|
gpu_memory_max_gb=_UNSET,
|
|
282
295
|
monitors=None, # list of dicts from create_monitor(); each captures a 3D ROI of the field
|
|
283
296
|
pseudo_propagation_distance: float = 0.0, # μm; 0 = off. Overrides max_iterations. C++ engine only.
|
|
@@ -329,6 +342,9 @@ def simulate_pro(
|
|
|
329
342
|
effective_strict = (
|
|
330
343
|
_defaults["strict_validation"] if strict_validation is None else bool(strict_validation)
|
|
331
344
|
)
|
|
345
|
+
effective_sampling = (
|
|
346
|
+
_defaults["sampling_warnings"] if sampling_warnings is None else bool(sampling_warnings)
|
|
347
|
+
)
|
|
332
348
|
effective_gpu_max = (
|
|
333
349
|
_defaults["gpu_memory_max_gb"] if gpu_memory_max_gb is _UNSET else gpu_memory_max_gb
|
|
334
350
|
)
|
|
@@ -391,6 +407,7 @@ def simulate_pro(
|
|
|
391
407
|
keep_residuals_list=full_residuals,
|
|
392
408
|
progress_callback=_progress_cb,
|
|
393
409
|
strict_validation=effective_strict,
|
|
410
|
+
sampling_warnings=effective_sampling,
|
|
394
411
|
gpu_memory_max_gb=effective_gpu_max,
|
|
395
412
|
monitors=monitors,
|
|
396
413
|
pseudo_propagation_distance=pseudo_propagation_distance,
|
|
@@ -53,6 +53,7 @@ def validate_parameters(
|
|
|
53
53
|
periodic: Optional[tuple] = None,
|
|
54
54
|
pseudo_propagation_distance: float = 0.0,
|
|
55
55
|
data_type: str = "RefractiveIndex",
|
|
56
|
+
sampling_warnings: bool = True, # False silences the "pixels per wavelength" advisory; Nyquist warnings stay
|
|
56
57
|
) -> List[str]:
|
|
57
58
|
"""
|
|
58
59
|
Validate simulation parameters before upload.
|
|
@@ -213,7 +214,7 @@ def validate_parameters(
|
|
|
213
214
|
if not anisotropic:
|
|
214
215
|
break
|
|
215
216
|
pixels_per_wavelength = wavelength / (n_max * max(pixel_sizes))
|
|
216
|
-
if pixels_per_wavelength < 10:
|
|
217
|
+
if pixels_per_wavelength < 10 and sampling_warnings:
|
|
217
218
|
warnings.append(
|
|
218
219
|
f"Only {pixels_per_wavelength:.1f} pixels per wavelength in the "
|
|
219
220
|
f"highest-index medium (n_max={n_max:.2f}) on the coarsest axis. Recommend >= 10 "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavesimpro
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -75,4 +75,38 @@ field = wp.read_binary_field(run.download("output.bin"))
|
|
|
75
75
|
- Preflight memory check and cost estimate before a run.
|
|
76
76
|
- Binary upload/download helpers for permittivity volumes, source fields and monitors.
|
|
77
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
|
+
|
|
78
112
|
See `CHANGELOG.md` for what each version added. Issues and feature requests: support@rayfos.com.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|