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.
Files changed (20) hide show
  1. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/PKG-INFO +1 -1
  2. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/pyproject.toml +1 -1
  3. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/__init__.py +1 -1
  4. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/client.py +96 -11
  5. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/execute.py +40 -0
  6. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro.egg-info/PKG-INFO +1 -1
  7. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/setup.cfg +0 -0
  8. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/__main__.py +0 -0
  9. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/binary_utils.py +0 -0
  10. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/config.py +0 -0
  11. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/exceptions.py +0 -0
  12. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/json_helper.py +0 -0
  13. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/py.typed +0 -0
  14. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/setup.py +0 -0
  15. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/simulate.py +0 -0
  16. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro/validate.py +0 -0
  17. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro.egg-info/SOURCES.txt +0 -0
  18. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
  19. {wavesimpro-0.14.0 → wavesimpro-0.15.1}/src/wavesimpro.egg-info/requires.txt +0 -0
  20. {wavesimpro-0.14.0 → wavesimpro-0.15.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: 0.14.0
3
+ Version: 0.15.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wavesimpro"
7
- version = "0.14.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"
@@ -49,7 +49,7 @@ from .json_helper import (
49
49
  parse_progress_data,
50
50
  )
51
51
 
52
- __version__ = "0.14.0"
52
+ __version__ = "0.15.1"
53
53
  __all__ = [
54
54
  # Client
55
55
  "RayfosClient",
@@ -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
- """Wrap a file-like object's read() with a tqdm progress bar.
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
- Returns a context-manager-friendly object. If tqdm is not installed, the
35
- original stream is returned and no progress is shown.
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
- return _tqdm.wrapattr(stream, "read", total=total_bytes, unit="B",
39
- unit_scale=True, unit_divisor=1024, desc=desc,
40
- miniters=1, mininterval=0.5)
41
- return stream
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
- wrapped = _wrap_with_progress(f, file_size, f"Upload {file_path.name}")
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
- wrapped = _wrap_with_progress(f, file_size, f"Upload {file_path.name}")
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavesimpro
3
- Version: 0.14.0
3
+ Version: 0.15.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
File without changes