wavesimpro 1.9.0__tar.gz → 1.10.1__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.9.0 → wavesimpro-1.10.1}/PKG-INFO +10 -3
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/README.md +9 -2
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/pyproject.toml +1 -1
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/__init__.py +20 -2
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/client.py +121 -172
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/json_helper.py +486 -9
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro.egg-info/PKG-INFO +10 -3
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro.egg-info/SOURCES.txt +2 -1
- wavesimpro-1.10.1/tests/test_command_wait.py +64 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/setup.cfg +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/__main__.py +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/binary_utils.py +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/config.py +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/exceptions.py +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/execute.py +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/py.typed +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/setup.py +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/simulate.py +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro/validate.py +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro.egg-info/requires.txt +0 -0
- {wavesimpro-1.9.0 → wavesimpro-1.10.1}/src/wavesimpro.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavesimpro
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.1
|
|
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
|
|
@@ -59,8 +59,15 @@ field = wp.read_binary_field(run.download("output.bin"))
|
|
|
59
59
|
|
|
60
60
|
## What the API covers
|
|
61
61
|
|
|
62
|
-
- Setup helpers for domains, media, primitives (box,
|
|
63
|
-
|
|
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.
|
|
64
71
|
- Per-axis pixel sizes (`pixel_size=[dx, dy, dz]`) and sub-pixel permittivity averaging
|
|
65
72
|
(`subpixel_factor=`), when the compute node supports them.
|
|
66
73
|
- Preflight memory check and cost estimate before a run.
|
|
@@ -36,8 +36,15 @@ field = wp.read_binary_field(run.download("output.bin"))
|
|
|
36
36
|
|
|
37
37
|
## What the API covers
|
|
38
38
|
|
|
39
|
-
- Setup helpers for domains, media, primitives (box,
|
|
40
|
-
|
|
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.
|
|
41
48
|
- Per-axis pixel sizes (`pixel_size=[dx, dy, dz]`) and sub-pixel permittivity averaging
|
|
42
49
|
(`subpixel_factor=`), when the compute node supports them.
|
|
43
50
|
- Preflight memory check and cost estimate before a run.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wavesimpro"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.10.1"
|
|
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"
|
|
@@ -6,7 +6,7 @@ supporting both cloud and local deployment modes, as well as direct interaction
|
|
|
6
6
|
with RayfosMachineService via its REST API.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from .client import RayfosClient
|
|
9
|
+
from .client import RayfosClient, command_status_name, is_terminal_command_status
|
|
10
10
|
from .config import ClientConfig
|
|
11
11
|
from .exceptions import (
|
|
12
12
|
RayfosAPIError,
|
|
@@ -33,6 +33,14 @@ 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,
|
|
@@ -46,10 +54,12 @@ from .json_helper import (
|
|
|
46
54
|
parse_progress_data,
|
|
47
55
|
)
|
|
48
56
|
|
|
49
|
-
__version__ = "1.
|
|
57
|
+
__version__ = "1.10.1"
|
|
50
58
|
__all__ = [
|
|
51
59
|
# Client
|
|
52
60
|
"RayfosClient",
|
|
61
|
+
"command_status_name",
|
|
62
|
+
"is_terminal_command_status",
|
|
53
63
|
"ClientConfig",
|
|
54
64
|
# Exceptions
|
|
55
65
|
"RayfosAPIError",
|
|
@@ -78,6 +88,14 @@ __all__ = [
|
|
|
78
88
|
"create_simulation_domain",
|
|
79
89
|
"create_stl_primitive",
|
|
80
90
|
"create_box_primitive",
|
|
91
|
+
"create_sphere_primitive",
|
|
92
|
+
"create_ellipsoid_primitive",
|
|
93
|
+
"create_cylinder_primitive",
|
|
94
|
+
"create_cone_primitive",
|
|
95
|
+
"create_prism_primitive",
|
|
96
|
+
"create_gds_primitive",
|
|
97
|
+
"create_gds_layer_config",
|
|
98
|
+
"create_gds_structure_override",
|
|
81
99
|
"create_medium",
|
|
82
100
|
"create_point_source",
|
|
83
101
|
"create_plane_wave_source",
|
|
@@ -77,6 +77,49 @@ def _make_progress_callback(total_bytes: int, desc: str):
|
|
|
77
77
|
logger = logging.getLogger(__name__)
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
|
|
81
|
+
# CommandStatus enum (C# RayfosCommandModels) by value; the server sends the NAME in "status"
|
|
82
|
+
# (JsonStringEnumConverter). Older servers sent the integer in "commandStatus".
|
|
83
|
+
_COMMAND_STATUS_BY_INT = {
|
|
84
|
+
0: 'New', 1: 'Assigned', 2: 'SentToMachine', 3: 'SendToMachineForceCancellation',
|
|
85
|
+
4: 'SendToMachineForceDeleteTask', 5: 'RequestedForceCancellationInMachine',
|
|
86
|
+
6: 'RequestedForceStopTaskInMachine', 7: 'Cancelled', 8: 'Deleted', 9: 'Running',
|
|
87
|
+
10: 'Completed', 11: 'Failed', 12: 'FailedRemovedFromMachine',
|
|
88
|
+
}
|
|
89
|
+
_TERMINAL_STATUSES = {'Cancelled', 'Deleted', 'Completed', 'Failed', 'FailedRemovedFromMachine'}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def command_status_name(status: Dict[str, Any]) -> str:
|
|
93
|
+
"""The command state name from a command-status dict, whatever the server version.
|
|
94
|
+
|
|
95
|
+
Reads ``status`` (current servers: the enum name, e.g. ``"Completed"``), falling back to the
|
|
96
|
+
legacy ``commandStatus`` / ``CommandStatus`` (integer or name). Returns ``"Unknown"`` when absent.
|
|
97
|
+
"""
|
|
98
|
+
if not isinstance(status, dict):
|
|
99
|
+
return 'Unknown'
|
|
100
|
+
for key in ('status', 'Status', 'commandStatus', 'CommandStatus'):
|
|
101
|
+
raw = status.get(key)
|
|
102
|
+
if raw is None or raw == '':
|
|
103
|
+
continue
|
|
104
|
+
if isinstance(raw, bool):
|
|
105
|
+
continue
|
|
106
|
+
if isinstance(raw, int):
|
|
107
|
+
return _COMMAND_STATUS_BY_INT.get(raw, f'Unknown({raw})')
|
|
108
|
+
text = str(raw).strip()
|
|
109
|
+
if text.isdigit():
|
|
110
|
+
return _COMMAND_STATUS_BY_INT.get(int(text), f'Unknown({text})')
|
|
111
|
+
for name in _COMMAND_STATUS_BY_INT.values():
|
|
112
|
+
if name.lower() == text.lower():
|
|
113
|
+
return name
|
|
114
|
+
return text
|
|
115
|
+
return 'Unknown'
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_terminal_command_status(name: str) -> bool:
|
|
119
|
+
"""True for Completed, Failed, FailedRemovedFromMachine, Cancelled and Deleted."""
|
|
120
|
+
return name in _TERMINAL_STATUSES
|
|
121
|
+
|
|
122
|
+
|
|
80
123
|
class RayfosClient:
|
|
81
124
|
"""
|
|
82
125
|
Client for interacting with RayfosJobServer API
|
|
@@ -740,9 +783,9 @@ class RayfosClient:
|
|
|
740
783
|
|
|
741
784
|
Example:
|
|
742
785
|
>>> version = client.get_project_version(project_id, version_id)
|
|
743
|
-
>>>
|
|
744
|
-
...
|
|
745
|
-
...
|
|
786
|
+
>>> for file_id in version.get('outputFileIds', []):
|
|
787
|
+
... info = client.get_file_info(project_id, version_id, file_id)
|
|
788
|
+
... print(f"Output file: {info.get('fileName')} ({info.get('fileSize')} bytes)")
|
|
746
789
|
"""
|
|
747
790
|
return self._get(f"/api/projects/{project_id}/versions/{version_id}")
|
|
748
791
|
|
|
@@ -1287,7 +1330,7 @@ class RayfosClient:
|
|
|
1287
1330
|
Returns:
|
|
1288
1331
|
File metadata dictionary
|
|
1289
1332
|
"""
|
|
1290
|
-
return self._get(f"/api/projects/{project_id}/versions/{version_id}/files/{file_id}")
|
|
1333
|
+
return self._get(f"/api/projects/{project_id}/versions/{version_id}/files/{file_id}/metadata")
|
|
1291
1334
|
|
|
1292
1335
|
# ========================================================================
|
|
1293
1336
|
# COMMAND OPERATIONS
|
|
@@ -1382,7 +1425,8 @@ class RayfosClient:
|
|
|
1382
1425
|
Ignored when using a local machine service URL.
|
|
1383
1426
|
|
|
1384
1427
|
Returns:
|
|
1385
|
-
Command status dictionary
|
|
1428
|
+
Command status dictionary. The command id is under ``"id"`` and, for convenience,
|
|
1429
|
+
``"commandId"``; the state is under ``"status"`` (read it with ``command_status_name``).
|
|
1386
1430
|
|
|
1387
1431
|
Example:
|
|
1388
1432
|
>>> command = client.create_command(project_id, version_id,
|
|
@@ -1390,9 +1434,17 @@ class RayfosClient:
|
|
|
1390
1434
|
>>> print(f"Command created: {command['id']}")
|
|
1391
1435
|
"""
|
|
1392
1436
|
if self.config.machine_service_url:
|
|
1393
|
-
|
|
1437
|
+
result = self._create_command_machine_service(project_id, version_id)
|
|
1394
1438
|
else:
|
|
1395
|
-
|
|
1439
|
+
result = self._create_command_cloud(project_id, version_id, engine_preference, use_gpu, preferred_machine_id)
|
|
1440
|
+
# The server names the command id "id"; other API calls and version records use "commandId".
|
|
1441
|
+
# Return both so either spelling works.
|
|
1442
|
+
if isinstance(result, dict):
|
|
1443
|
+
cid = result.get('id') or result.get('commandId') or result.get('Id')
|
|
1444
|
+
if cid:
|
|
1445
|
+
result.setdefault('id', cid)
|
|
1446
|
+
result.setdefault('commandId', cid)
|
|
1447
|
+
return result
|
|
1396
1448
|
|
|
1397
1449
|
def _create_command_machine_service(
|
|
1398
1450
|
self,
|
|
@@ -1439,35 +1491,8 @@ class RayfosClient:
|
|
|
1439
1491
|
|
|
1440
1492
|
@staticmethod
|
|
1441
1493
|
def _parse_command_status(status_raw) -> str:
|
|
1442
|
-
"""
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
Args:
|
|
1446
|
-
status_raw: Either integer enum value or string name
|
|
1447
|
-
|
|
1448
|
-
Returns:
|
|
1449
|
-
String name of the status
|
|
1450
|
-
"""
|
|
1451
|
-
# CommandStatus enum from C# RayfosCommandModels
|
|
1452
|
-
COMMAND_STATUS_MAP = {
|
|
1453
|
-
0: 'New',
|
|
1454
|
-
1: 'Assigned',
|
|
1455
|
-
2: 'SentToMachine',
|
|
1456
|
-
3: 'SendToMachineForceCancellation',
|
|
1457
|
-
4: 'SendToMachineForceDeleteTask',
|
|
1458
|
-
5: 'RequestedForceCancellationInMachine',
|
|
1459
|
-
6: 'RequestedForceStopTaskInMachine',
|
|
1460
|
-
7: 'Cancelled',
|
|
1461
|
-
8: 'Deleted',
|
|
1462
|
-
9: 'Running',
|
|
1463
|
-
10: 'Completed',
|
|
1464
|
-
11: 'Failed',
|
|
1465
|
-
12: 'FailedRemovedFromMachine'
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
if isinstance(status_raw, int):
|
|
1469
|
-
return COMMAND_STATUS_MAP.get(status_raw, f'Unknown({status_raw})')
|
|
1470
|
-
return status_raw
|
|
1494
|
+
"""Command state name from an integer (legacy ``commandStatus``) or a name (current ``status``)."""
|
|
1495
|
+
return command_status_name({"status": status_raw})
|
|
1471
1496
|
|
|
1472
1497
|
@staticmethod
|
|
1473
1498
|
def _parse_progress_data(progress_obj) -> Dict[str, Any]:
|
|
@@ -1542,16 +1567,16 @@ class RayfosClient:
|
|
|
1542
1567
|
command_id: Command UUID
|
|
1543
1568
|
|
|
1544
1569
|
Returns:
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1570
|
+
Command-status dict: ``id``, ``status`` (enum name such as ``"Running"`` or
|
|
1571
|
+
``"Completed"``), ``statusText``, ``progressValue``. Older servers sent
|
|
1572
|
+
the state as the integer ``commandStatus``; :func:`command_status_name` reads either.
|
|
1573
|
+
|
|
1548
1574
|
Example:
|
|
1549
1575
|
>>> status = client.get_command_status("command-uuid")
|
|
1550
|
-
>>>
|
|
1551
|
-
>>> print(f"
|
|
1552
|
-
>>>
|
|
1553
|
-
|
|
1554
|
-
... print(f"Output files: {status.get('outputFileIds', [])}")
|
|
1576
|
+
>>> name = command_status_name(status) # "Running", "Completed", ...
|
|
1577
|
+
>>> print(f"{name}: {status.get('statusText')} ({status.get('progressValue', 0)}%)")
|
|
1578
|
+
>>> if name == "Completed":
|
|
1579
|
+
... files = client.get_project_version(project_id, version_id)["outputFileIds"]
|
|
1555
1580
|
"""
|
|
1556
1581
|
# Always call RayfosJobServer directly for command status (user endpoint)
|
|
1557
1582
|
# Machine service is only for file operations, not command status
|
|
@@ -1561,113 +1586,73 @@ class RayfosClient:
|
|
|
1561
1586
|
self,
|
|
1562
1587
|
command_id: str,
|
|
1563
1588
|
poll_interval: float = 2.0,
|
|
1564
|
-
timeout: Optional[float] =
|
|
1589
|
+
timeout: Optional[float] = 24 * 3600,
|
|
1565
1590
|
progress_callback: Optional[callable] = None
|
|
1566
1591
|
) -> Dict[str, Any]:
|
|
1567
1592
|
"""
|
|
1568
|
-
Wait for a command to
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1593
|
+
Wait for a command to finish (synchronous execution).
|
|
1594
|
+
|
|
1595
|
+
Polls :meth:`get_command_status` until the command reaches a terminal state and returns the
|
|
1596
|
+
final status dict on ``Completed``. The state is read with :func:`command_status_name`, which
|
|
1597
|
+
understands both the current ``status`` field (enum name) and the legacy ``commandStatus``
|
|
1598
|
+
integer, so this works against every server version.
|
|
1599
|
+
|
|
1573
1600
|
Args:
|
|
1574
|
-
command_id: Command UUID
|
|
1575
|
-
poll_interval: Seconds between status checks (default
|
|
1576
|
-
timeout: Maximum seconds to wait
|
|
1577
|
-
progress_callback: Optional callback(status_dict) called on each
|
|
1578
|
-
|
|
1601
|
+
command_id: Command UUID (``create_command(...)["id"]``, also available as ``["commandId"]``)
|
|
1602
|
+
poll_interval: Seconds between status checks (default 2.0)
|
|
1603
|
+
timeout: Maximum seconds to wait (default 24 h; ``None`` waits forever)
|
|
1604
|
+
progress_callback: Optional ``callback(status_dict)`` called on each poll
|
|
1605
|
+
|
|
1579
1606
|
Returns:
|
|
1580
|
-
Final
|
|
1581
|
-
|
|
1607
|
+
Final command-status dict (``status == "Completed"``). To get the result files, list the
|
|
1608
|
+
version's ``outputFileIds`` (``get_project_version(project_id, version_id)["outputFileIds"]``,
|
|
1609
|
+
then ``download_file``); the ``outputFileIds`` field
|
|
1610
|
+
of the status is not filled by current servers.
|
|
1611
|
+
|
|
1582
1612
|
Raises:
|
|
1583
|
-
TimeoutError:
|
|
1584
|
-
RayfosAPIError:
|
|
1585
|
-
|
|
1613
|
+
TimeoutError: the command did not finish within ``timeout`` seconds
|
|
1614
|
+
RayfosAPIError: the command failed, was cancelled or was deleted
|
|
1615
|
+
|
|
1586
1616
|
Example:
|
|
1587
|
-
>>>
|
|
1588
|
-
>>>
|
|
1589
|
-
|
|
1590
|
-
>>> # Wait for completion with progress updates
|
|
1591
|
-
>>> def show_progress(status):
|
|
1592
|
-
... print(f"Progress: {status.get('progressValue', 0)}% - {status.get('statusText', 'Running')}")
|
|
1593
|
-
>>>
|
|
1594
|
-
>>> final_status = client.wait_for_command_completion(
|
|
1595
|
-
... command['id'],
|
|
1596
|
-
... progress_callback=show_progress,
|
|
1597
|
-
... timeout=3600 # 1 hour timeout
|
|
1598
|
-
... )
|
|
1599
|
-
>>> print(f"Command completed! Output files: {final_status.get('outputFileIds', [])}")
|
|
1617
|
+
>>> command = client.create_command(project_id, version_id)
|
|
1618
|
+
>>> final = client.wait_for_command_completion(command["id"], timeout=3600,
|
|
1619
|
+
... progress_callback=lambda s: print(command_status_name(s), s.get("statusText")))
|
|
1600
1620
|
"""
|
|
1601
1621
|
import time
|
|
1602
|
-
|
|
1603
|
-
# CommandStatus enum values from C# (serialized as integers)
|
|
1604
|
-
# New=0, Assigned=1, SentToMachine=2, etc.
|
|
1605
|
-
COMMAND_STATUS = {
|
|
1606
|
-
0: 'New',
|
|
1607
|
-
1: 'Assigned',
|
|
1608
|
-
2: 'SentToMachine',
|
|
1609
|
-
3: 'SendToMachineForceCancellation',
|
|
1610
|
-
4: 'SendToMachineForceDeleteTask',
|
|
1611
|
-
5: 'RequestedForceCancellationInMachine',
|
|
1612
|
-
6: 'RequestedForceStopTaskInMachine',
|
|
1613
|
-
7: 'Cancelled',
|
|
1614
|
-
8: 'Deleted',
|
|
1615
|
-
9: 'Running',
|
|
1616
|
-
10: 'Completed',
|
|
1617
|
-
11: 'Failed',
|
|
1618
|
-
12: 'FailedRemovedFromMachine'
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
# Terminal states (by integer value and name)
|
|
1622
|
-
TERMINAL_STATUS_INTS = {7, 8, 10, 11, 12} # Cancelled, Deleted, Completed, Failed, FailedRemovedFromMachine
|
|
1623
|
-
TERMINAL_STATUS_NAMES = {'Cancelled', 'Deleted', 'Completed', 'Failed', 'FailedRemovedFromMachine'}
|
|
1624
|
-
|
|
1622
|
+
|
|
1625
1623
|
start_time = time.time()
|
|
1626
|
-
|
|
1627
1624
|
while True:
|
|
1628
|
-
# Check timeout
|
|
1629
|
-
if timeout and (time.time() - start_time) > timeout:
|
|
1630
|
-
raise TimeoutError(f"Command {command_id} did not complete within {timeout} seconds")
|
|
1631
|
-
|
|
1632
|
-
# Get current status
|
|
1633
1625
|
status = self.get_command_status(command_id)
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
# Handle both integer (from C# enum) and string formats
|
|
1637
|
-
if isinstance(command_status_raw, int):
|
|
1638
|
-
command_status = COMMAND_STATUS.get(command_status_raw, f'Unknown({command_status_raw})')
|
|
1639
|
-
is_terminal = command_status_raw in TERMINAL_STATUS_INTS
|
|
1640
|
-
else:
|
|
1641
|
-
command_status = command_status_raw
|
|
1642
|
-
is_terminal = command_status in TERMINAL_STATUS_NAMES
|
|
1643
|
-
|
|
1644
|
-
# Call progress callback if provided
|
|
1626
|
+
name = command_status_name(status)
|
|
1627
|
+
|
|
1645
1628
|
if progress_callback:
|
|
1646
1629
|
progress_callback(status)
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
if command_status in ('Completed', 10):
|
|
1630
|
+
|
|
1631
|
+
if is_terminal_command_status(name):
|
|
1632
|
+
if name == 'Completed':
|
|
1651
1633
|
return status
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
raise RayfosAPIError(f"Command {command_id} failed: {
|
|
1655
|
-
|
|
1634
|
+
detail = status.get('statusText') or ''
|
|
1635
|
+
if name in ('Failed', 'FailedRemovedFromMachine'):
|
|
1636
|
+
raise RayfosAPIError(f"Command {command_id} failed: {detail or name}")
|
|
1637
|
+
if name == 'Cancelled':
|
|
1656
1638
|
raise RayfosAPIError(f"Command {command_id} was cancelled")
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1639
|
+
raise RayfosAPIError(f"Command {command_id} was deleted")
|
|
1640
|
+
|
|
1641
|
+
if timeout is not None and (time.time() - start_time) > timeout:
|
|
1642
|
+
raise TimeoutError(f"Command {command_id} did not finish within {timeout:.0f} s (last status: {name})")
|
|
1643
|
+
|
|
1661
1644
|
time.sleep(poll_interval)
|
|
1662
|
-
|
|
1645
|
+
|
|
1663
1646
|
def create_command_and_wait(
|
|
1664
1647
|
self,
|
|
1665
1648
|
project_id: str,
|
|
1666
1649
|
version_id: str,
|
|
1667
1650
|
local_machine_service_id: Optional[str] = None,
|
|
1668
1651
|
poll_interval: float = 2.0,
|
|
1669
|
-
timeout: Optional[float] =
|
|
1670
|
-
progress_callback: Optional[callable] = None
|
|
1652
|
+
timeout: Optional[float] = 24 * 3600,
|
|
1653
|
+
progress_callback: Optional[callable] = None,
|
|
1654
|
+
engine_preference: str = 'Any',
|
|
1655
|
+
use_gpu: Optional[bool] = None,
|
|
1671
1656
|
) -> Dict[str, Any]:
|
|
1672
1657
|
"""
|
|
1673
1658
|
Create a command and wait for it to complete (synchronous execution)
|
|
@@ -1706,8 +1691,11 @@ class RayfosClient:
|
|
|
1706
1691
|
... client.download_file(project_id, version_id, file_id, f"output/{file_id}")
|
|
1707
1692
|
"""
|
|
1708
1693
|
# Create the command (async)
|
|
1709
|
-
|
|
1710
|
-
|
|
1694
|
+
# local_machine_service_id pins the run to that compute node (it used to be passed, by
|
|
1695
|
+
# position, as the engine preference, which the server rejects).
|
|
1696
|
+
command_status = self.create_command(project_id, version_id, engine_preference=engine_preference,
|
|
1697
|
+
use_gpu=use_gpu, preferred_machine_id=local_machine_service_id)
|
|
1698
|
+
command_id = command_status.get('id') or command_status.get('commandId')
|
|
1711
1699
|
|
|
1712
1700
|
if not command_id:
|
|
1713
1701
|
raise RayfosAPIError("Failed to create command: no command ID returned")
|
|
@@ -1898,46 +1886,7 @@ class RayfosClient:
|
|
|
1898
1886
|
)
|
|
1899
1887
|
return self._handle_response(response)
|
|
1900
1888
|
|
|
1901
|
-
def parse_command_status(self, status_value
|
|
1902
|
-
"""
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
Args:
|
|
1906
|
-
status_value: Integer status code from CommandStatusDto
|
|
1907
|
-
|
|
1908
|
-
Returns:
|
|
1909
|
-
Human-readable status name
|
|
1910
|
-
|
|
1911
|
-
CommandStatus enum values from C# RayfosCommandModels:
|
|
1912
|
-
0 = New
|
|
1913
|
-
1 = Assigned
|
|
1914
|
-
2 = SentToMachine
|
|
1915
|
-
3 = SendToMachineForceCancellation
|
|
1916
|
-
4 = SendToMachineForceDeleteTask
|
|
1917
|
-
5 = RequestedForceCancellationInMachine
|
|
1918
|
-
6 = RequestedForceStopTaskInMachine
|
|
1919
|
-
7 = Cancelled
|
|
1920
|
-
8 = Deleted
|
|
1921
|
-
9 = Running
|
|
1922
|
-
10 = Completed
|
|
1923
|
-
11 = Failed
|
|
1924
|
-
12 = FailedRemovedFromMachine
|
|
1925
|
-
"""
|
|
1926
|
-
status_map = {
|
|
1927
|
-
0: "New",
|
|
1928
|
-
1: "Assigned",
|
|
1929
|
-
2: "SentToMachine",
|
|
1930
|
-
3: "SendToMachineForceCancellation",
|
|
1931
|
-
4: "SendToMachineForceDeleteTask",
|
|
1932
|
-
5: "RequestedForceCancellationInMachine",
|
|
1933
|
-
6: "RequestedForceStopTaskInMachine",
|
|
1934
|
-
7: "Cancelled",
|
|
1935
|
-
8: "Deleted",
|
|
1936
|
-
9: "Running",
|
|
1937
|
-
10: "Completed",
|
|
1938
|
-
11: "Failed",
|
|
1939
|
-
12: "FailedRemovedFromMachine",
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
return status_map.get(status_value, f"Unknown({status_value})")
|
|
1889
|
+
def parse_command_status(self, status_value) -> str:
|
|
1890
|
+
"""Command state name from an integer (legacy ``commandStatus``) or a name (current ``status``)."""
|
|
1891
|
+
return command_status_name({"status": status_value})
|
|
1943
1892
|
|
|
@@ -42,6 +42,84 @@ def create_simulation_domain(
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Primitive helpers
|
|
47
|
+
#
|
|
48
|
+
# Every primitive dict mirrors RayfosCommandModels.PrimitiveBase (Primitives.cs):
|
|
49
|
+
# Id (uuid), Name, ShapeType (enum serialised as its name, e.g. "Cylinder"),
|
|
50
|
+
# MediumId, Position {X,Y,Z: Length}, PositionAnchor (enum name), Rotation
|
|
51
|
+
# {X,Y,Z} in degrees, plus the per-shape properties. Lengths are {Value, Unit}.
|
|
52
|
+
# "PositioningMode": "CenterAndSize" is the Studio bookkeeping key the existing
|
|
53
|
+
# helpers already emit (ignored by the server).
|
|
54
|
+
#
|
|
55
|
+
# Sub-pixel flag ("SubPixel"): True = this primitive's interface voxels are
|
|
56
|
+
# probe-averaged (create_wavesim_properties' subpixel_factor probes per axis);
|
|
57
|
+
# False = centre-sampled staircase for this primitive; None (default) = key
|
|
58
|
+
# omitted, the primitive follows the simulation-level PermittivitySampling.Mode
|
|
59
|
+
# (the pre-1.10 behaviour).
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
_POSITION_ANCHORS = (
|
|
63
|
+
"Center", "BottomLeftFront", "BottomCenter", "BottomRightFront", "BottomLeftBack",
|
|
64
|
+
"BottomRightBack", "TopCenter", "TopLeftFront", "TopRightFront", "TopLeftBack", "TopRightBack",
|
|
65
|
+
)
|
|
66
|
+
# Anchors each shape's GetCanonicalCenterPosition() accepts (others throw on the server).
|
|
67
|
+
_BOX_ANCHORS = ("Center", "BottomLeftFront", "BottomCenter", "TopCenter")
|
|
68
|
+
_AXIAL_ANCHORS = ("Center", "BottomCenter", "TopCenter") # Cylinder, Cone, Prism
|
|
69
|
+
_AXES = ("X", "Y", "Z")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _check_anchor(anchor: str, allowed: Tuple[str, ...], shape: str) -> str:
|
|
73
|
+
a = str(anchor)
|
|
74
|
+
if a not in allowed:
|
|
75
|
+
raise ValueError(f"position_anchor for {shape} must be one of {list(allowed)}, got {anchor!r}")
|
|
76
|
+
return a
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _check_axis(axis: str) -> str:
|
|
80
|
+
a = str(axis).upper()
|
|
81
|
+
if a not in _AXES:
|
|
82
|
+
raise ValueError(f"axis must be 'X', 'Y' or 'Z', got {axis!r}")
|
|
83
|
+
return a
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _length(value: float, unit: str) -> Dict[str, Any]:
|
|
87
|
+
return {"Value": float(value), "Unit": unit}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _primitive_base(
|
|
91
|
+
shape_type: str,
|
|
92
|
+
name: str,
|
|
93
|
+
medium_id: str,
|
|
94
|
+
x: float,
|
|
95
|
+
y: float,
|
|
96
|
+
z: float,
|
|
97
|
+
rotation_x: float,
|
|
98
|
+
rotation_y: float,
|
|
99
|
+
rotation_z: float,
|
|
100
|
+
position_anchor: str,
|
|
101
|
+
unit: str,
|
|
102
|
+
subpixel: Optional[bool],
|
|
103
|
+
) -> Dict[str, Any]:
|
|
104
|
+
prim: Dict[str, Any] = {
|
|
105
|
+
"Id": str(uuid.uuid4()),
|
|
106
|
+
"Name": name,
|
|
107
|
+
"ShapeType": shape_type,
|
|
108
|
+
"MediumId": medium_id,
|
|
109
|
+
"PositioningMode": "CenterAndSize",
|
|
110
|
+
"Position": {"X": _length(x, unit), "Y": _length(y, unit), "Z": _length(z, unit)},
|
|
111
|
+
"PositionAnchor": position_anchor,
|
|
112
|
+
"Rotation": {"X": float(rotation_x), "Y": float(rotation_y), "Z": float(rotation_z)},
|
|
113
|
+
}
|
|
114
|
+
_apply_subpixel(prim, subpixel)
|
|
115
|
+
return prim
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _apply_subpixel(prim: Dict[str, Any], subpixel: Optional[bool]) -> None:
|
|
119
|
+
if subpixel is not None:
|
|
120
|
+
prim["SubPixel"] = bool(subpixel)
|
|
121
|
+
|
|
122
|
+
|
|
45
123
|
def create_stl_primitive(
|
|
46
124
|
name: str,
|
|
47
125
|
file_id: str,
|
|
@@ -55,7 +133,8 @@ def create_stl_primitive(
|
|
|
55
133
|
scale_x: float = 1.0,
|
|
56
134
|
scale_y: float = 1.0,
|
|
57
135
|
scale_z: float = 1.0,
|
|
58
|
-
unit: str = "Micrometers"
|
|
136
|
+
unit: str = "Micrometers",
|
|
137
|
+
subpixel: Optional[bool] = None,
|
|
59
138
|
) -> Dict[str, Any]:
|
|
60
139
|
"""
|
|
61
140
|
Create STL primitive structure
|
|
@@ -68,8 +147,10 @@ def create_stl_primitive(
|
|
|
68
147
|
rotation_x, rotation_y, rotation_z: Rotation angles in degrees
|
|
69
148
|
scale_x, scale_y, scale_z: Scale factors
|
|
70
149
|
unit: Unit of measurement
|
|
150
|
+
subpixel: True = sub-pixel average this mesh's interface voxels, False = staircase,
|
|
151
|
+
None (default) = key omitted, follow the simulation-level setting.
|
|
71
152
|
"""
|
|
72
|
-
|
|
153
|
+
prim = {
|
|
73
154
|
"Id": str(uuid.uuid4()),
|
|
74
155
|
"Name": name,
|
|
75
156
|
"ShapeType": "StlModel",
|
|
@@ -94,6 +175,8 @@ def create_stl_primitive(
|
|
|
94
175
|
},
|
|
95
176
|
"_tempStlData": {}
|
|
96
177
|
}
|
|
178
|
+
_apply_subpixel(prim, subpixel)
|
|
179
|
+
return prim
|
|
97
180
|
|
|
98
181
|
|
|
99
182
|
def create_box_primitive(
|
|
@@ -105,19 +188,46 @@ def create_box_primitive(
|
|
|
105
188
|
size_x: float,
|
|
106
189
|
size_y: float,
|
|
107
190
|
size_z: float,
|
|
108
|
-
unit: str = "Micrometers"
|
|
191
|
+
unit: str = "Micrometers",
|
|
192
|
+
subpixel: Optional[bool] = None,
|
|
193
|
+
position_anchor: str = "Center",
|
|
194
|
+
rotation_x: float = 0.0,
|
|
195
|
+
rotation_y: float = 0.0,
|
|
196
|
+
rotation_z: float = 0.0,
|
|
109
197
|
) -> Dict[str, Any]:
|
|
110
|
-
"""
|
|
111
|
-
|
|
198
|
+
"""
|
|
199
|
+
Create a box primitive (BoxPrimitive, CenterAndSize representation).
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
name: Primitive name
|
|
203
|
+
medium_id: Medium/Material ID
|
|
204
|
+
center_x, center_y, center_z: Position (what it refers to is set by position_anchor)
|
|
205
|
+
size_x, size_y, size_z: Full extents along X/Y/Z (Size.Width/Height/Depth)
|
|
206
|
+
unit: Unit of measurement for position and size
|
|
207
|
+
subpixel: True = sub-pixel average this box's interface voxels, False = staircase,
|
|
208
|
+
None (default) = key omitted, follow the simulation-level setting.
|
|
209
|
+
position_anchor: "Center" (default), "BottomLeftFront" (minimum corner),
|
|
210
|
+
"BottomCenter" (centre of the -Y face) or "TopCenter" (centre of the +Y face).
|
|
211
|
+
The server rejects any other anchor for a box.
|
|
212
|
+
rotation_x, rotation_y, rotation_z: Rotation in degrees
|
|
213
|
+
"""
|
|
214
|
+
anchor = _check_anchor(position_anchor, _BOX_ANCHORS, "Box")
|
|
215
|
+
prim: Dict[str, Any] = {
|
|
216
|
+
"Id": str(uuid.uuid4()),
|
|
217
|
+
# "Type" kept for older servers; "ShapeType" is the canonical discriminator (same value).
|
|
112
218
|
"Type": "Box",
|
|
219
|
+
"ShapeType": "Box",
|
|
113
220
|
"Name": name,
|
|
114
221
|
"MediumId": medium_id,
|
|
115
222
|
"PositioningMode": "CenterAndSize",
|
|
223
|
+
"Representation": "CenterAndSize",
|
|
116
224
|
"Position": {
|
|
117
225
|
"X": {"Value": center_x, "Unit": unit},
|
|
118
226
|
"Y": {"Value": center_y, "Unit": unit},
|
|
119
227
|
"Z": {"Value": center_z, "Unit": unit}
|
|
120
228
|
},
|
|
229
|
+
"PositionAnchor": anchor,
|
|
230
|
+
"Rotation": {"X": float(rotation_x), "Y": float(rotation_y), "Z": float(rotation_z)},
|
|
121
231
|
# Size3D on the server is Width/Height/Depth (X/Y/Z keys were silently ignored → 4 µm default).
|
|
122
232
|
"Size": {
|
|
123
233
|
"Width": {"Value": size_x, "Unit": unit},
|
|
@@ -125,6 +235,338 @@ def create_box_primitive(
|
|
|
125
235
|
"Depth": {"Value": size_z, "Unit": unit}
|
|
126
236
|
}
|
|
127
237
|
}
|
|
238
|
+
_apply_subpixel(prim, subpixel)
|
|
239
|
+
return prim
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def create_sphere_primitive(
|
|
243
|
+
name: str,
|
|
244
|
+
medium_id: str,
|
|
245
|
+
center_x: float,
|
|
246
|
+
center_y: float,
|
|
247
|
+
center_z: float,
|
|
248
|
+
radius: float,
|
|
249
|
+
rotation_x: float = 0.0,
|
|
250
|
+
rotation_y: float = 0.0,
|
|
251
|
+
rotation_z: float = 0.0,
|
|
252
|
+
position_anchor: str = "Center",
|
|
253
|
+
unit: str = "Micrometers",
|
|
254
|
+
subpixel: Optional[bool] = None,
|
|
255
|
+
) -> Dict[str, Any]:
|
|
256
|
+
"""
|
|
257
|
+
Create a sphere primitive (SpherePrimitive).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
name: Primitive name
|
|
261
|
+
medium_id: Medium/Material ID
|
|
262
|
+
center_x, center_y, center_z: Sphere centre
|
|
263
|
+
radius: Radius (Length in ``unit``)
|
|
264
|
+
rotation_x, rotation_y, rotation_z: Rotation in degrees (no effect on a sphere)
|
|
265
|
+
position_anchor: Stored as given (default "Center"); the server always treats
|
|
266
|
+
Position as the centre of a sphere.
|
|
267
|
+
unit: Unit of measurement
|
|
268
|
+
subpixel: True / False / None (follow the simulation-level setting)
|
|
269
|
+
"""
|
|
270
|
+
anchor = _check_anchor(position_anchor, _POSITION_ANCHORS, "Sphere")
|
|
271
|
+
prim = _primitive_base("Sphere", name, medium_id, center_x, center_y, center_z,
|
|
272
|
+
rotation_x, rotation_y, rotation_z, anchor, unit, subpixel)
|
|
273
|
+
prim["Radius"] = _length(radius, unit)
|
|
274
|
+
return prim
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def create_ellipsoid_primitive(
|
|
278
|
+
name: str,
|
|
279
|
+
medium_id: str,
|
|
280
|
+
center_x: float,
|
|
281
|
+
center_y: float,
|
|
282
|
+
center_z: float,
|
|
283
|
+
radius_x: float,
|
|
284
|
+
radius_y: float,
|
|
285
|
+
radius_z: float,
|
|
286
|
+
rotation_x: float = 0.0,
|
|
287
|
+
rotation_y: float = 0.0,
|
|
288
|
+
rotation_z: float = 0.0,
|
|
289
|
+
position_anchor: str = "Center",
|
|
290
|
+
unit: str = "Micrometers",
|
|
291
|
+
subpixel: Optional[bool] = None,
|
|
292
|
+
) -> Dict[str, Any]:
|
|
293
|
+
"""
|
|
294
|
+
Create an ellipsoid primitive (EllipsoidPrimitive).
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
name: Primitive name
|
|
298
|
+
medium_id: Medium/Material ID
|
|
299
|
+
center_x, center_y, center_z: Ellipsoid centre
|
|
300
|
+
radius_x, radius_y, radius_z: Semi-axes along X/Y/Z (before rotation)
|
|
301
|
+
rotation_x, rotation_y, rotation_z: Rotation in degrees
|
|
302
|
+
position_anchor: Stored as given (default "Center"); the server always treats
|
|
303
|
+
Position as the centre of an ellipsoid.
|
|
304
|
+
unit: Unit of measurement
|
|
305
|
+
subpixel: True / False / None (follow the simulation-level setting)
|
|
306
|
+
"""
|
|
307
|
+
anchor = _check_anchor(position_anchor, _POSITION_ANCHORS, "Ellipsoid")
|
|
308
|
+
prim = _primitive_base("Ellipsoid", name, medium_id, center_x, center_y, center_z,
|
|
309
|
+
rotation_x, rotation_y, rotation_z, anchor, unit, subpixel)
|
|
310
|
+
prim["RadiusX"] = _length(radius_x, unit)
|
|
311
|
+
prim["RadiusY"] = _length(radius_y, unit)
|
|
312
|
+
prim["RadiusZ"] = _length(radius_z, unit)
|
|
313
|
+
return prim
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def create_cylinder_primitive(
|
|
317
|
+
name: str,
|
|
318
|
+
medium_id: str,
|
|
319
|
+
position_x: float,
|
|
320
|
+
position_y: float,
|
|
321
|
+
position_z: float,
|
|
322
|
+
radius: float,
|
|
323
|
+
height: float,
|
|
324
|
+
axis: str = "Z",
|
|
325
|
+
rotation_x: float = 0.0,
|
|
326
|
+
rotation_y: float = 0.0,
|
|
327
|
+
rotation_z: float = 0.0,
|
|
328
|
+
position_anchor: str = "BottomCenter",
|
|
329
|
+
unit: str = "Micrometers",
|
|
330
|
+
subpixel: Optional[bool] = None,
|
|
331
|
+
) -> Dict[str, Any]:
|
|
332
|
+
"""
|
|
333
|
+
Create a cylinder primitive (CylinderPrimitive).
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
name: Primitive name
|
|
337
|
+
medium_id: Medium/Material ID
|
|
338
|
+
position_x, position_y, position_z: Anchor point (see position_anchor)
|
|
339
|
+
radius: Radius
|
|
340
|
+
height: Extent along ``axis``
|
|
341
|
+
axis: "X", "Y" or "Z" (default "Z")
|
|
342
|
+
rotation_x, rotation_y, rotation_z: Rotation in degrees
|
|
343
|
+
position_anchor: "BottomCenter" (default, as the server and Studio): Position is the
|
|
344
|
+
centre of the base face and the cylinder extends +height along ``axis``;
|
|
345
|
+
"Center": Position is the geometric centre; "TopCenter": centre of the top face.
|
|
346
|
+
unit: Unit of measurement
|
|
347
|
+
subpixel: True / False / None (follow the simulation-level setting)
|
|
348
|
+
"""
|
|
349
|
+
anchor = _check_anchor(position_anchor, _AXIAL_ANCHORS, "Cylinder")
|
|
350
|
+
prim = _primitive_base("Cylinder", name, medium_id, position_x, position_y, position_z,
|
|
351
|
+
rotation_x, rotation_y, rotation_z, anchor, unit, subpixel)
|
|
352
|
+
prim["Radius"] = _length(radius, unit)
|
|
353
|
+
prim["Height"] = _length(height, unit)
|
|
354
|
+
prim["Axis"] = _check_axis(axis)
|
|
355
|
+
return prim
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def create_cone_primitive(
|
|
359
|
+
name: str,
|
|
360
|
+
medium_id: str,
|
|
361
|
+
position_x: float,
|
|
362
|
+
position_y: float,
|
|
363
|
+
position_z: float,
|
|
364
|
+
radius: float,
|
|
365
|
+
height: float,
|
|
366
|
+
axis: str = "Z",
|
|
367
|
+
rotation_x: float = 0.0,
|
|
368
|
+
rotation_y: float = 0.0,
|
|
369
|
+
rotation_z: float = 0.0,
|
|
370
|
+
position_anchor: str = "BottomCenter",
|
|
371
|
+
unit: str = "Micrometers",
|
|
372
|
+
subpixel: Optional[bool] = None,
|
|
373
|
+
) -> Dict[str, Any]:
|
|
374
|
+
"""
|
|
375
|
+
Create a cone primitive (ConePrimitive). The base is a disc of ``radius``; the apex
|
|
376
|
+
lies ``height`` further along +``axis``.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
name: Primitive name
|
|
380
|
+
medium_id: Medium/Material ID
|
|
381
|
+
position_x, position_y, position_z: Anchor point (see position_anchor)
|
|
382
|
+
radius: Base radius
|
|
383
|
+
height: Base-to-apex distance along ``axis``
|
|
384
|
+
axis: "X", "Y" or "Z" (default "Z")
|
|
385
|
+
rotation_x, rotation_y, rotation_z: Rotation in degrees
|
|
386
|
+
position_anchor: "BottomCenter" (default, as the server and Studio): Position is the
|
|
387
|
+
centre of the base; "Center": Position is the CENTROID (height/4 above the base,
|
|
388
|
+
not height/2); "TopCenter": Position is the apex.
|
|
389
|
+
unit: Unit of measurement
|
|
390
|
+
subpixel: True / False / None (follow the simulation-level setting)
|
|
391
|
+
"""
|
|
392
|
+
anchor = _check_anchor(position_anchor, _AXIAL_ANCHORS, "Cone")
|
|
393
|
+
prim = _primitive_base("Cone", name, medium_id, position_x, position_y, position_z,
|
|
394
|
+
rotation_x, rotation_y, rotation_z, anchor, unit, subpixel)
|
|
395
|
+
prim["Radius"] = _length(radius, unit)
|
|
396
|
+
prim["Height"] = _length(height, unit)
|
|
397
|
+
prim["Axis"] = _check_axis(axis)
|
|
398
|
+
return prim
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def create_prism_primitive(
|
|
402
|
+
name: str,
|
|
403
|
+
medium_id: str,
|
|
404
|
+
position_x: float,
|
|
405
|
+
position_y: float,
|
|
406
|
+
position_z: float,
|
|
407
|
+
radius: float,
|
|
408
|
+
height: float,
|
|
409
|
+
number_of_sides: int = 6,
|
|
410
|
+
axis: str = "Z",
|
|
411
|
+
top_scale: float = 1.0,
|
|
412
|
+
twist_degrees: float = 0.0,
|
|
413
|
+
rotation_x: float = 0.0,
|
|
414
|
+
rotation_y: float = 0.0,
|
|
415
|
+
rotation_z: float = 0.0,
|
|
416
|
+
position_anchor: str = "BottomCenter",
|
|
417
|
+
unit: str = "Micrometers",
|
|
418
|
+
subpixel: Optional[bool] = None,
|
|
419
|
+
) -> Dict[str, Any]:
|
|
420
|
+
"""
|
|
421
|
+
Create a regular N-sided prism primitive (PrismPrimitive): a regular polygon with
|
|
422
|
+
``number_of_sides`` corners on a circle of ``radius`` extruded ``height`` along ``axis``.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
name: Primitive name
|
|
426
|
+
medium_id: Medium/Material ID
|
|
427
|
+
position_x, position_y, position_z: Anchor point (see position_anchor)
|
|
428
|
+
radius: Circumradius of the bottom polygon
|
|
429
|
+
height: Extent along ``axis``
|
|
430
|
+
number_of_sides: Polygon corners, integer >= 3 (3 = triangular prism, 4 = square,
|
|
431
|
+
large N approaches a cylinder). Default 6.
|
|
432
|
+
axis: "X", "Y" or "Z" (default "Z")
|
|
433
|
+
top_scale: Top-ring radius factor: 1 = straight prism (default), 0 = pyramid,
|
|
434
|
+
> 1 = flared
|
|
435
|
+
twist_degrees: Rotation of the top ring about ``axis`` in degrees (default 0)
|
|
436
|
+
rotation_x, rotation_y, rotation_z: Rotation in degrees
|
|
437
|
+
position_anchor: "BottomCenter" (default, as the server and Studio), "Center" or
|
|
438
|
+
"TopCenter" (±height/2 along ``axis``, as for a cylinder).
|
|
439
|
+
unit: Unit of measurement
|
|
440
|
+
subpixel: True / False / None (follow the simulation-level setting)
|
|
441
|
+
"""
|
|
442
|
+
anchor = _check_anchor(position_anchor, _AXIAL_ANCHORS, "Prism")
|
|
443
|
+
n = int(number_of_sides)
|
|
444
|
+
if n != number_of_sides or n < 3:
|
|
445
|
+
raise ValueError(f"number_of_sides must be an integer >= 3, got {number_of_sides!r}")
|
|
446
|
+
if top_scale < 0:
|
|
447
|
+
raise ValueError(f"top_scale must be >= 0, got {top_scale!r}")
|
|
448
|
+
prim = _primitive_base("Prism", name, medium_id, position_x, position_y, position_z,
|
|
449
|
+
rotation_x, rotation_y, rotation_z, anchor, unit, subpixel)
|
|
450
|
+
prim["Radius"] = _length(radius, unit)
|
|
451
|
+
prim["Height"] = _length(height, unit)
|
|
452
|
+
prim["Axis"] = _check_axis(axis)
|
|
453
|
+
prim["NumberOfSides"] = n
|
|
454
|
+
prim["TopScale"] = float(top_scale)
|
|
455
|
+
prim["TwistDegrees"] = float(twist_degrees)
|
|
456
|
+
return prim
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def create_gds_layer_config(
|
|
460
|
+
layer_number: int,
|
|
461
|
+
medium_id: Optional[str] = None,
|
|
462
|
+
height_um: Optional[float] = None,
|
|
463
|
+
z_offset_um: Optional[float] = None,
|
|
464
|
+
) -> Dict[str, Any]:
|
|
465
|
+
"""
|
|
466
|
+
Per-layer settings for create_gds_primitive(layer_configs=...) (GdsLayerConfig).
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
layer_number: GDS layer number
|
|
470
|
+
medium_id: Medium for this layer; None = the GDS primitive's medium_id
|
|
471
|
+
height_um: Extrusion thickness in micrometres; None = the primitive's layer_height_um
|
|
472
|
+
z_offset_um: Bottom of the layer in micrometres relative to the primitive;
|
|
473
|
+
None = auto-stacked
|
|
474
|
+
"""
|
|
475
|
+
cfg: Dict[str, Any] = {"LayerNumber": int(layer_number)}
|
|
476
|
+
if medium_id is not None:
|
|
477
|
+
cfg["MediumId"] = medium_id
|
|
478
|
+
if height_um is not None:
|
|
479
|
+
cfg["Height"] = float(height_um)
|
|
480
|
+
if z_offset_um is not None:
|
|
481
|
+
cfg["ZOffset"] = float(z_offset_um)
|
|
482
|
+
return cfg
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def create_gds_structure_override(
|
|
486
|
+
structure_name: str,
|
|
487
|
+
layer_overrides: Optional[List[Dict[str, Any]]] = None,
|
|
488
|
+
) -> Dict[str, Any]:
|
|
489
|
+
"""
|
|
490
|
+
Per-structure (GDS cell) overrides for create_gds_primitive(structure_overrides=...)
|
|
491
|
+
(GdsStructureConfig). ``layer_overrides`` is a list built with create_gds_layer_config()
|
|
492
|
+
(same keys as GdsStructureLayerOverride: LayerNumber, MediumId, Height, ZOffset); unset
|
|
493
|
+
fields inherit from the primitive's layer_configs.
|
|
494
|
+
"""
|
|
495
|
+
cfg: Dict[str, Any] = {"StructureName": structure_name}
|
|
496
|
+
if layer_overrides is not None:
|
|
497
|
+
cfg["LayerOverrides"] = list(layer_overrides)
|
|
498
|
+
return cfg
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def create_gds_primitive(
|
|
502
|
+
name: str,
|
|
503
|
+
file_id: str,
|
|
504
|
+
medium_id: str,
|
|
505
|
+
center_x: float = 0.0,
|
|
506
|
+
center_y: float = 0.0,
|
|
507
|
+
center_z: float = 0.0,
|
|
508
|
+
layer_height_um: float = 1.0,
|
|
509
|
+
selected_layers: Optional[List[int]] = None,
|
|
510
|
+
layer_configs: Optional[List[Dict[str, Any]]] = None,
|
|
511
|
+
structure_overrides: Optional[List[Dict[str, Any]]] = None,
|
|
512
|
+
metadata_file_id: Optional[str] = None,
|
|
513
|
+
voxelization_method: str = "ViaStl",
|
|
514
|
+
rotation_x: float = 0.0,
|
|
515
|
+
rotation_y: float = 0.0,
|
|
516
|
+
rotation_z: float = 0.0,
|
|
517
|
+
scale_x: float = 1.0,
|
|
518
|
+
scale_y: float = 1.0,
|
|
519
|
+
scale_z: float = 1.0,
|
|
520
|
+
position_anchor: str = "Center",
|
|
521
|
+
unit: str = "Micrometers",
|
|
522
|
+
subpixel: Optional[bool] = None,
|
|
523
|
+
) -> Dict[str, Any]:
|
|
524
|
+
"""
|
|
525
|
+
Create a GDSII layout primitive (GdsModelPrimitive) from an uploaded .gds file.
|
|
526
|
+
|
|
527
|
+
The layout's polygons are extruded per layer; Position is the centre of the top
|
|
528
|
+
structure's bounding box. Layer thickness and offsets are plain numbers in
|
|
529
|
+
MICROMETRES on the server model (not Length objects), whatever ``unit`` is.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
name: Primitive name
|
|
533
|
+
file_id: File id of the uploaded GDS file (GdsDataId), as for create_stl_primitive
|
|
534
|
+
medium_id: Default medium for every layer
|
|
535
|
+
center_x, center_y, center_z: Position of the layout centre
|
|
536
|
+
layer_height_um: Default extrusion thickness of each layer in µm (LayerHeight)
|
|
537
|
+
selected_layers: Layer numbers to include; None = all layers
|
|
538
|
+
layer_configs: Per-layer settings from create_gds_layer_config(); None = every layer
|
|
539
|
+
uses medium_id / layer_height_um (legacy mode)
|
|
540
|
+
structure_overrides: Per-structure (cell) overrides from
|
|
541
|
+
create_gds_structure_override(); None = none
|
|
542
|
+
metadata_file_id: Optional uploaded layer-metadata YML file id (GdsMetadataId)
|
|
543
|
+
voxelization_method: "ViaStl" (default) or "Direct"
|
|
544
|
+
rotation_x, rotation_y, rotation_z: Rotation in degrees
|
|
545
|
+
scale_x, scale_y, scale_z: Scale factors
|
|
546
|
+
position_anchor: Stored as given (default "Center"); the server treats Position as
|
|
547
|
+
the layout centre whatever the anchor.
|
|
548
|
+
unit: Unit of Position
|
|
549
|
+
subpixel: True / False / None (follow the simulation-level setting)
|
|
550
|
+
"""
|
|
551
|
+
anchor = _check_anchor(position_anchor, _POSITION_ANCHORS, "GdsModel")
|
|
552
|
+
method = str(voxelization_method)
|
|
553
|
+
if method not in ("ViaStl", "Direct"):
|
|
554
|
+
raise ValueError(f"voxelization_method must be 'ViaStl' or 'Direct', got {voxelization_method!r}")
|
|
555
|
+
prim = _primitive_base("GdsModel", name, medium_id, center_x, center_y, center_z,
|
|
556
|
+
rotation_x, rotation_y, rotation_z, anchor, unit, subpixel)
|
|
557
|
+
prim["GdsDataId"] = file_id
|
|
558
|
+
prim["ScaleFactors"] = {"X": float(scale_x), "Y": float(scale_y), "Z": float(scale_z)}
|
|
559
|
+
prim["LayerHeight"] = float(layer_height_um)
|
|
560
|
+
prim["VoxelizationMethod"] = method
|
|
561
|
+
if metadata_file_id is not None:
|
|
562
|
+
prim["GdsMetadataId"] = metadata_file_id
|
|
563
|
+
if selected_layers is not None:
|
|
564
|
+
prim["SelectedLayers"] = [int(n) for n in selected_layers]
|
|
565
|
+
if layer_configs is not None:
|
|
566
|
+
prim["LayerConfigs"] = list(layer_configs)
|
|
567
|
+
if structure_overrides is not None:
|
|
568
|
+
prim["StructureOverrides"] = list(structure_overrides)
|
|
569
|
+
return prim
|
|
128
570
|
|
|
129
571
|
|
|
130
572
|
def create_medium(
|
|
@@ -189,9 +631,8 @@ def create_point_source(
|
|
|
189
631
|
return {
|
|
190
632
|
"Id": str(uuid.uuid4()),
|
|
191
633
|
"Name": name,
|
|
192
|
-
"Id": str(uuid.uuid4()),
|
|
193
634
|
"SourceType": "Point",
|
|
194
|
-
"
|
|
635
|
+
"IsEnabled": True,
|
|
195
636
|
"UseInternalPythonSource": False,
|
|
196
637
|
"Position": {
|
|
197
638
|
"X": {"Value": position_x, "Unit": unit},
|
|
@@ -357,7 +798,9 @@ def create_voxels_model_primitive(
|
|
|
357
798
|
grid_nz: int = 32,
|
|
358
799
|
data_format: str = "Float32",
|
|
359
800
|
data_type: str = "RefractiveIndex",
|
|
360
|
-
unit: str = "Micrometers"
|
|
801
|
+
unit: str = "Micrometers",
|
|
802
|
+
subpixel: Optional[bool] = None,
|
|
803
|
+
antialias_width: Optional[float] = None,
|
|
361
804
|
) -> Dict[str, Any]:
|
|
362
805
|
"""
|
|
363
806
|
Create VoxelsModel primitive for arbitrary refractive index distributions
|
|
@@ -372,6 +815,19 @@ def create_voxels_model_primitive(
|
|
|
372
815
|
data_format: 'Float32', 'Float64', 'Complex64', 'Complex128'
|
|
373
816
|
data_type: 'RefractiveIndex', 'ComplexRefractiveIndex', 'Permittivity', 'ComplexPermittivity'
|
|
374
817
|
unit: Unit of measurement
|
|
818
|
+
|
|
819
|
+
subpixel: True anti-aliases the array on the service:
|
|
820
|
+
- array at the simulation voxel size: softened IN PLACE by a normalised Gaussian of
|
|
821
|
+
``antialias_width`` voxels on the permittivity (same resolution, volume kept).
|
|
822
|
+
On reference shapes it halves the interface error vs true sub-pixel values.
|
|
823
|
+
- array finer by a whole number per axis (voxel size = simulation voxel / k): each
|
|
824
|
+
k_x x k_y x k_z block is averaged into one simulation voxel (the same rule as the
|
|
825
|
+
shapes' sub-pixel probes); cells the array only partly covers blend with what
|
|
826
|
+
lies underneath.
|
|
827
|
+
None/False: the array must be at the simulation voxel size and is copied voxel for
|
|
828
|
+
voxel. The simulation-level ``subpixel_factor`` does not apply to voxel arrays.
|
|
829
|
+
antialias_width: Gaussian width (standard deviation, voxels) for the in-place case;
|
|
830
|
+
None = 0.5 (best on average); thin waveguides prefer ~0.7. Range 0.1-3.
|
|
375
831
|
"""
|
|
376
832
|
return {
|
|
377
833
|
"ShapeType": "VoxelsModel",
|
|
@@ -395,7 +851,9 @@ def create_voxels_model_primitive(
|
|
|
395
851
|
},
|
|
396
852
|
"DataFormat": data_format,
|
|
397
853
|
"DataType": data_type,
|
|
398
|
-
"Rotation": {"X": 0, "Y": 0, "Z": 0}
|
|
854
|
+
"Rotation": {"X": 0, "Y": 0, "Z": 0},
|
|
855
|
+
**({"SubPixel": bool(subpixel)} if subpixel is not None else {}),
|
|
856
|
+
**({"AntiAliasWidth": float(antialias_width)} if antialias_width is not None else {}),
|
|
399
857
|
}
|
|
400
858
|
|
|
401
859
|
|
|
@@ -701,6 +1159,16 @@ def create_wavesim_properties(
|
|
|
701
1159
|
derives the iteration count from this distance and it OVERRIDES
|
|
702
1160
|
residual_norm_max_iterations (threshold still stops the run early).
|
|
703
1161
|
C++ engine only.
|
|
1162
|
+
subpixel_factor: Sub-pixel probe count per axis, s (1..64, 16 is the reference
|
|
1163
|
+
value). It is used by every primitive whose SubPixel is true; a primitive with
|
|
1164
|
+
no explicit flag follows the simulation-level mode, which this argument sets to
|
|
1165
|
+
SubPixel (s > 1) — so passing it alone smooths every primitive, STL and GDSII
|
|
1166
|
+
(the pre-1.10 behaviour). Per-primitive flags (``subpixel=True`` on the
|
|
1167
|
+
primitive helpers) are the preferred way to choose which shapes are smoothed.
|
|
1168
|
+
When this is None but some primitive carries an explicit flag, the factor is
|
|
1169
|
+
emitted with Mode "Staircase" and s = 16, so flagged primitives have a probe
|
|
1170
|
+
count and unflagged ones stay staircased. VoxelsModel arrays are never
|
|
1171
|
+
re-sampled.
|
|
704
1172
|
"""
|
|
705
1173
|
props = {
|
|
706
1174
|
"SimulationDomain": simulation_domain,
|
|
@@ -764,6 +1232,8 @@ def create_wavesim_properties(
|
|
|
764
1232
|
# Staircase. The service probes each voxel of primitives / STL / GDSII at
|
|
765
1233
|
# subpixel_factor points per axis and stores the mean permittivity; it does NOT
|
|
766
1234
|
# re-sample a VoxelsModel array (average that yourself before upload).
|
|
1235
|
+
# Mode is the default for primitives without an explicit "SubPixel" flag; flagged
|
|
1236
|
+
# primitives decide for themselves and use Factor as their probe count.
|
|
767
1237
|
if subpixel_factor is not None:
|
|
768
1238
|
s = int(subpixel_factor)
|
|
769
1239
|
if s < 1 or s > 64:
|
|
@@ -774,6 +1244,13 @@ def create_wavesim_properties(
|
|
|
774
1244
|
"Rule": "ArithmeticMean",
|
|
775
1245
|
"InterfaceOnly": True,
|
|
776
1246
|
}
|
|
1247
|
+
elif any(isinstance(p, dict) and "SubPixel" in p for p in (primitives or [])):
|
|
1248
|
+
props["PermittivitySampling"] = {
|
|
1249
|
+
"Mode": "Staircase",
|
|
1250
|
+
"Factor": 16,
|
|
1251
|
+
"Rule": "ArithmeticMean",
|
|
1252
|
+
"InterfaceOnly": True,
|
|
1253
|
+
}
|
|
777
1254
|
|
|
778
1255
|
return props
|
|
779
1256
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavesimpro
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.1
|
|
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
|
|
@@ -59,8 +59,15 @@ field = wp.read_binary_field(run.download("output.bin"))
|
|
|
59
59
|
|
|
60
60
|
## What the API covers
|
|
61
61
|
|
|
62
|
-
- Setup helpers for domains, media, primitives (box,
|
|
63
|
-
|
|
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.
|
|
64
71
|
- Per-axis pixel sizes (`pixel_size=[dx, dy, dz]`) and sub-pixel permittivity averaging
|
|
65
72
|
(`subpixel_factor=`), when the compute node supports them.
|
|
66
73
|
- Preflight memory check and cost estimate before a run.
|
|
@@ -16,4 +16,5 @@ src/wavesimpro.egg-info/PKG-INFO
|
|
|
16
16
|
src/wavesimpro.egg-info/SOURCES.txt
|
|
17
17
|
src/wavesimpro.egg-info/dependency_links.txt
|
|
18
18
|
src/wavesimpro.egg-info/requires.txt
|
|
19
|
-
src/wavesimpro.egg-info/top_level.txt
|
|
19
|
+
src/wavesimpro.egg-info/top_level.txt
|
|
20
|
+
tests/test_command_wait.py
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""wait_for_command_completion must work with both server shapes of the command status:
|
|
2
|
+
current servers send the state NAME in "status"; older ones sent an integer "commandStatus"."""
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from wavesimpro import RayfosClient, command_status_name, is_terminal_command_status
|
|
6
|
+
from wavesimpro.exceptions import RayfosAPIError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FakeClient(RayfosClient):
|
|
10
|
+
"""RayfosClient whose HTTP layer is replaced by a scripted sequence of status replies."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, replies, created=None): # noqa: D401 - no network setup
|
|
13
|
+
self._replies = list(replies)
|
|
14
|
+
self._created = created
|
|
15
|
+
self.created_with = None
|
|
16
|
+
|
|
17
|
+
def get_command_status(self, command_id):
|
|
18
|
+
return self._replies.pop(0) if len(self._replies) > 1 else self._replies[0]
|
|
19
|
+
|
|
20
|
+
def create_command(self, project_id, version_id, engine_preference='Any', use_gpu=None, preferred_machine_id=None):
|
|
21
|
+
self.created_with = dict(engine_preference=engine_preference, use_gpu=use_gpu, preferred_machine_id=preferred_machine_id)
|
|
22
|
+
return dict(self._created)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_current_server_shape_returns_on_completed():
|
|
26
|
+
c = FakeClient([{"id": "c1", "status": "Running"}, {"id": "c1", "status": "Completed", "outputFileIds": ["f"]}])
|
|
27
|
+
final = c.wait_for_command_completion("c1", poll_interval=0, timeout=5)
|
|
28
|
+
assert final["outputFileIds"] == ["f"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_legacy_integer_shape_still_works():
|
|
32
|
+
c = FakeClient([{"commandStatus": 9}, {"commandStatus": 10}])
|
|
33
|
+
assert c.wait_for_command_completion("c1", poll_interval=0, timeout=5)["commandStatus"] == 10
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize("state", ["Failed", "FailedRemovedFromMachine", "Cancelled", "Deleted"])
|
|
37
|
+
def test_terminal_failures_raise(state):
|
|
38
|
+
c = FakeClient([{"status": state, "statusText": "boom"}])
|
|
39
|
+
with pytest.raises(RayfosAPIError):
|
|
40
|
+
c.wait_for_command_completion("c1", poll_interval=0, timeout=5)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_timeout_names_the_last_state():
|
|
44
|
+
c = FakeClient([{"status": "Running"}])
|
|
45
|
+
with pytest.raises(TimeoutError, match="Running"):
|
|
46
|
+
c.wait_for_command_completion("c1", poll_interval=0.01, timeout=0.05)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_status_helpers():
|
|
50
|
+
assert command_status_name({"status": "Completed"}) == "Completed"
|
|
51
|
+
assert command_status_name({"status": "completed"}) == "Completed"
|
|
52
|
+
assert command_status_name({"commandStatus": 11}) == "Failed"
|
|
53
|
+
assert command_status_name({"commandStatus": "10"}) == "Completed"
|
|
54
|
+
assert command_status_name({"status": "", "commandStatus": 9}) == "Running"
|
|
55
|
+
assert command_status_name({}) == "Unknown"
|
|
56
|
+
assert is_terminal_command_status("Completed") and not is_terminal_command_status("Running")
|
|
57
|
+
assert RayfosClient._parse_command_status("Running") == "Running"
|
|
58
|
+
assert RayfosClient._parse_command_status(10) == "Completed"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_create_command_and_wait_pins_the_machine_and_accepts_either_id_key():
|
|
62
|
+
c = FakeClient([{"status": "Completed"}], created={"commandId": "c9", "status": "New"})
|
|
63
|
+
c.create_command_and_wait("p", "v", local_machine_service_id="node-1", poll_interval=0, timeout=5)
|
|
64
|
+
assert c.created_with == {"engine_preference": "Any", "use_gpu": None, "preferred_machine_id": "node-1"}
|
|
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
|