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.
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/PKG-INFO +37 -2
- wavesimpro-1.17.0/README.md +89 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/pyproject.toml +1 -1
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/__init__.py +2 -1
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/client.py +111 -3
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/execute.py +866 -848
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/json_helper.py +1469 -1336
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/runs.py +63 -7
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/simulate.py +21 -0
- wavesimpro-1.17.0/src/wavesimpro/studio_mode.py +161 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/validate.py +2 -1
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/PKG-INFO +37 -2
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/SOURCES.txt +5 -1
- wavesimpro-1.17.0/tests/test_boundary_profile.py +33 -0
- wavesimpro-1.17.0/tests/test_engine_mode.py +64 -0
- wavesimpro-1.17.0/tests/test_studio_mode.py +100 -0
- wavesimpro-1.10.2/README.md +0 -54
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/setup.cfg +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/__main__.py +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/binary_utils.py +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/config.py +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/exceptions.py +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/model.py +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/py.typed +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro/setup.py +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/requires.txt +0 -0
- {wavesimpro-1.10.2 → wavesimpro-1.17.0}/src/wavesimpro.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
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
|