wavesimpro 0.14.0__tar.gz → 0.15.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-0.14.0 → wavesimpro-0.15.1}/PKG-INFO +1 -1
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/pyproject.toml +1 -1
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/__init__.py +1 -1
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/client.py +96 -11
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/execute.py +40 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro.egg-info/PKG-INFO +1 -1
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/setup.cfg +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/__main__.py +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/binary_utils.py +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/config.py +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/exceptions.py +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/json_helper.py +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/py.typed +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/setup.py +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/simulate.py +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/validate.py +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro.egg-info/SOURCES.txt +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro.egg-info/requires.txt +0 -0
- {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wavesimpro"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.15.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"
|
|
@@ -4,6 +4,7 @@ Main client class for interacting with RayfosJobServer
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import logging
|
|
7
|
+
from contextlib import contextmanager
|
|
7
8
|
from typing import Dict, List, Optional, Any, BinaryIO
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
import requests
|
|
@@ -28,17 +29,27 @@ except ImportError:
|
|
|
28
29
|
_HAVE_TQDM = False
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
@contextmanager
|
|
31
33
|
def _wrap_with_progress(stream, total_bytes: int, desc: str):
|
|
32
|
-
"""
|
|
34
|
+
"""Context manager that wraps a file-like object's read() with a tqdm
|
|
35
|
+
progress bar and yields the wrapped stream.
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
If tqdm is not installed (or total is unknown), the original stream is
|
|
38
|
+
yielded and no progress is shown.
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
with _wrap_with_progress(f, size, "Upload foo") as wrapped:
|
|
42
|
+
requests.put(url, data=wrapped, ...)
|
|
36
43
|
"""
|
|
37
44
|
if _HAVE_TQDM and total_bytes > 0:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
# tqdm.wrapattr is itself a context manager whose __enter__ returns
|
|
46
|
+
# the wrapped stream — re-yield that so callers get a file-like object.
|
|
47
|
+
with _tqdm.wrapattr(stream, "read", total=total_bytes, unit="B",
|
|
48
|
+
unit_scale=True, unit_divisor=1024, desc=desc,
|
|
49
|
+
miniters=1, mininterval=0.5) as wrapped:
|
|
50
|
+
yield wrapped
|
|
51
|
+
else:
|
|
52
|
+
yield stream
|
|
42
53
|
|
|
43
54
|
|
|
44
55
|
def _make_progress_callback(total_bytes: int, desc: str):
|
|
@@ -266,6 +277,80 @@ class RayfosClient:
|
|
|
266
277
|
# PROJECT MANAGEMENT METHODS
|
|
267
278
|
# ========================================================================
|
|
268
279
|
|
|
280
|
+
def preflight_check(
|
|
281
|
+
self,
|
|
282
|
+
domain_um: tuple,
|
|
283
|
+
voxel_um: float,
|
|
284
|
+
boundary_um: float = 0.0,
|
|
285
|
+
periodic: tuple = (False, False, False),
|
|
286
|
+
engine_preference: str = "Any",
|
|
287
|
+
use_gpu: Optional[bool] = None,
|
|
288
|
+
) -> Dict[str, Any]:
|
|
289
|
+
"""
|
|
290
|
+
Ask the server (cloud or machine-service) whether a simulation of the
|
|
291
|
+
given shape will fit on the available compute targets, BEFORE uploading
|
|
292
|
+
any files.
|
|
293
|
+
|
|
294
|
+
This calls POST /api/preflightCheck. The same response shape is returned
|
|
295
|
+
regardless of which server answers (cloud aggregator vs single machine).
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
domain_um: (size_x, size_y, size_z) simulation domain extent in μm
|
|
299
|
+
voxel_um: voxel size in μm
|
|
300
|
+
boundary_um: PML / absorbing-boundary thickness in μm
|
|
301
|
+
periodic: (periodic_x, periodic_y, periodic_z) — periodic axes don't get boundary padding
|
|
302
|
+
engine_preference: "Any" | "Python" | "Cpp"
|
|
303
|
+
use_gpu: None (auto), True (require GPU), False (force CPU)
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
PreflightCheckResponse dict with keys:
|
|
307
|
+
- status: "Ok" | "Warning" | "Error"
|
|
308
|
+
- description: human-readable summary
|
|
309
|
+
- estimate: { nx, ny, nz, paddedNx, ..., totalMemoryGB, ... }
|
|
310
|
+
- machines: [{ machineId, gpuMemoryGB, cpuMemoryGB, fitsOnGpu, fitsOnCpu, ... }]
|
|
311
|
+
- recommendedMachineId
|
|
312
|
+
- suggestedAdjustments: [str, ...]
|
|
313
|
+
|
|
314
|
+
The minimal WavesimProperties payload is built here (no primitives, no
|
|
315
|
+
sources) since the estimator only needs domain + voxel + boundary + periodic.
|
|
316
|
+
"""
|
|
317
|
+
size_x, size_y, size_z = domain_um
|
|
318
|
+
periodic_x, periodic_y, periodic_z = periodic
|
|
319
|
+
body = {
|
|
320
|
+
"wavesimProperties": {
|
|
321
|
+
"simulationDomain": {
|
|
322
|
+
"positioningMode": "CenterAndSize",
|
|
323
|
+
"position": {
|
|
324
|
+
"x": {"value": 0.0, "unit": "Micrometers"},
|
|
325
|
+
"y": {"value": 0.0, "unit": "Micrometers"},
|
|
326
|
+
"z": {"value": 0.0, "unit": "Micrometers"},
|
|
327
|
+
},
|
|
328
|
+
"size": {
|
|
329
|
+
"width": {"value": float(size_x), "unit": "Micrometers"},
|
|
330
|
+
"height": {"value": float(size_y), "unit": "Micrometers"},
|
|
331
|
+
"depth": {"value": float(size_z), "unit": "Micrometers"},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
"voxelSize": {"value": float(voxel_um), "unit": "Micrometers"},
|
|
335
|
+
"boundarySize": {"value": float(boundary_um), "unit": "Micrometers"},
|
|
336
|
+
"periodicX": bool(periodic_x),
|
|
337
|
+
"periodicY": bool(periodic_y),
|
|
338
|
+
"periodicZ": bool(periodic_z),
|
|
339
|
+
},
|
|
340
|
+
"enginePreference": engine_preference,
|
|
341
|
+
"useGpu": use_gpu,
|
|
342
|
+
}
|
|
343
|
+
# Machine service URL takes precedence (local single-machine preflight); else cloud aggregator.
|
|
344
|
+
if self.config.machine_service_url:
|
|
345
|
+
url = self.config.get_machine_service_url("/api/preflightCheck")
|
|
346
|
+
verify = self.config.verify_ssl_machine_service
|
|
347
|
+
else:
|
|
348
|
+
url = self.config.get_api_url("/api/preflightCheck")
|
|
349
|
+
verify = self.config.verify_ssl
|
|
350
|
+
logger.info(f"POST {url} [preflight]")
|
|
351
|
+
response = self.session.post(url, json=body, timeout=self.config.timeout, verify=verify)
|
|
352
|
+
return self._handle_response(response)
|
|
353
|
+
|
|
269
354
|
def get_projects(self) -> List[Dict[str, Any]]:
|
|
270
355
|
"""
|
|
271
356
|
Get all projects for the authenticated user
|
|
@@ -542,8 +627,8 @@ class RayfosClient:
|
|
|
542
627
|
"x-ms-blob-type": "BlockBlob",
|
|
543
628
|
"Content-Length": str(file_size),
|
|
544
629
|
}
|
|
545
|
-
with open(file_path, "rb") as f
|
|
546
|
-
|
|
630
|
+
with open(file_path, "rb") as f, \
|
|
631
|
+
_wrap_with_progress(f, file_size, f"Upload {file_path.name}") as wrapped:
|
|
547
632
|
upload_response = requests.put(
|
|
548
633
|
sas_url,
|
|
549
634
|
data=wrapped,
|
|
@@ -584,8 +669,8 @@ class RayfosClient:
|
|
|
584
669
|
endpoint = f"/api/files/projects/{project_id}/versions/{version_id}/upload"
|
|
585
670
|
|
|
586
671
|
file_size = file_path.stat().st_size
|
|
587
|
-
with open(file_path, "rb") as f
|
|
588
|
-
|
|
672
|
+
with open(file_path, "rb") as f, \
|
|
673
|
+
_wrap_with_progress(f, file_size, f"Upload {file_path.name}") as wrapped:
|
|
589
674
|
files = {
|
|
590
675
|
"file": (file_path.name, wrapped, "application/octet-stream")
|
|
591
676
|
}
|
|
@@ -200,6 +200,46 @@ def execute_via_api(
|
|
|
200
200
|
verify_ssl_machine_service=verify_ssl_machine,
|
|
201
201
|
)
|
|
202
202
|
|
|
203
|
+
# 5b. Preflight check — abort BEFORE creating a project / uploading anything
|
|
204
|
+
# if the simulation won't fit on the available compute targets.
|
|
205
|
+
domain_x_um = float(nx) * float(pixel_size)
|
|
206
|
+
domain_y_um = float(ny) * float(pixel_size)
|
|
207
|
+
domain_z_um = float(nz) * float(pixel_size)
|
|
208
|
+
try:
|
|
209
|
+
pre = client.preflight_check(
|
|
210
|
+
domain_um=(domain_x_um, domain_y_um, domain_z_um),
|
|
211
|
+
voxel_um=float(pixel_size),
|
|
212
|
+
boundary_um=float(boundary_width) * float(pixel_size),
|
|
213
|
+
periodic=(bool(periodic[0]), bool(periodic[1]), bool(periodic[2])),
|
|
214
|
+
engine_preference=engine_preference,
|
|
215
|
+
use_gpu=use_gpu,
|
|
216
|
+
)
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
logger.warning(f"Preflight check failed (server may not support /api/preflightCheck yet): {exc}. Proceeding without it.")
|
|
219
|
+
pre = None
|
|
220
|
+
|
|
221
|
+
if pre is not None:
|
|
222
|
+
status = (pre.get("status") or "").lower()
|
|
223
|
+
description = pre.get("description") or ""
|
|
224
|
+
est = pre.get("estimate") or {}
|
|
225
|
+
logger.info(
|
|
226
|
+
f"Preflight: status={status} "
|
|
227
|
+
f"est_total={est.get('totalMemoryGB', 0):.2f} GB "
|
|
228
|
+
f"(E-field {est.get('efieldMemoryGB', 0):.2f} GB + workspace {est.get('workspaceMemoryGB', 0):.2f} GB) "
|
|
229
|
+
f"grid={est.get('nx')}x{est.get('ny')}x{est.get('nz')}"
|
|
230
|
+
)
|
|
231
|
+
logger.info(f"Preflight: {description}")
|
|
232
|
+
for s in pre.get("suggestedAdjustments") or []:
|
|
233
|
+
logger.info(f" Suggestion: {s}")
|
|
234
|
+
if status == "error":
|
|
235
|
+
raise RuntimeError(
|
|
236
|
+
f"Preflight rejected the simulation: {description} "
|
|
237
|
+
f"(no upload was attempted). "
|
|
238
|
+
f"Suggested adjustments: {pre.get('suggestedAdjustments') or '(none)'}"
|
|
239
|
+
)
|
|
240
|
+
if status == "warning":
|
|
241
|
+
logger.warning(f"Preflight warning: {description}")
|
|
242
|
+
|
|
203
243
|
# 6. Create or reuse project/version
|
|
204
244
|
auto_create = not project_id or not version_id
|
|
205
245
|
if auto_create:
|
|
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
|