wavesimpro 1.0.0__tar.gz → 1.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavesimpro
3
- Version: 1.0.0
3
+ Version: 1.0.2
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 = "1.0.0"
7
+ version = "1.0.2"
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"
@@ -45,7 +45,7 @@ from .json_helper import (
45
45
  parse_progress_data,
46
46
  )
47
47
 
48
- __version__ = "1.0.0"
48
+ __version__ = "1.0.2"
49
49
  __all__ = [
50
50
  # Client
51
51
  "RayfosClient",
@@ -7,13 +7,15 @@ command submission, polling, result download.
7
7
  """
8
8
 
9
9
  import os
10
+ import re
10
11
  import json
11
12
  import math
12
13
  import time
14
+ import shutil
13
15
  import logging
14
16
  import tempfile
15
17
  from pathlib import Path
16
- from typing import Tuple, Optional, Callable, Dict, Any, List
18
+ from typing import Tuple, Optional, Callable, Dict, Any, List, Union
17
19
 
18
20
  import numpy as np
19
21
 
@@ -44,6 +46,21 @@ def _extract_file_id(file_info: dict) -> str:
44
46
  raise KeyError(f"No file ID found in upload response. Keys: {list(file_info.keys())}")
45
47
 
46
48
 
49
+ def _monitor_output_filename(monitor_name: str) -> str:
50
+ """Expected on-server filename for a monitor's result.
51
+
52
+ Mirrors RayfosMachineService MonitorDataExtractor.BuildMonitorFileName: strip
53
+ characters illegal in a filename, drop a leading ``monitor[ _-]+`` token (so a
54
+ monitor named "Monitor_0" yields ``monitor_0.bin``, not ``monitor_Monitor_0.bin``),
55
+ then prefix ``monitor_`` and append ``.bin``.
56
+ """
57
+ core = "".join(c for c in (monitor_name or "") if c not in '<>:"/\\|?*') or "unnamed"
58
+ stripped = re.sub(r"^monitor[ _\-]+", "", core, flags=re.IGNORECASE)
59
+ if stripped:
60
+ core = stripped
61
+ return f"monitor_{core}.bin"
62
+
63
+
47
64
  def execute_via_api(
48
65
  refractive_index: np.ndarray,
49
66
  source,
@@ -71,7 +88,7 @@ def execute_via_api(
71
88
  strict_validation: bool = False,
72
89
  gpu_memory_max_gb: Optional[float] = None,
73
90
  monitors: Optional[List[Dict[str, Any]]] = None,
74
- ) -> Tuple[np.ndarray, bool]:
91
+ ) -> Tuple[Union[np.ndarray, List[np.ndarray]], bool]:
75
92
  """
76
93
  Execute a Wavesim simulation via the Rayfos REST API.
77
94
 
@@ -102,10 +119,17 @@ def execute_via_api(
102
119
  Ignored when machine_service_url is set (local mode always uses that machine).
103
120
  use_gpu: GPU selection — None (auto, default), True (require GPU), False (force CPU).
104
121
  Ignored when machine_service_url is set.
122
+ monitors: Optional list of monitor dicts (from create_monitor()). Each captures a 3D
123
+ ROI sub-cube of the field. When monitors are given, the server uploads only the
124
+ monitor_<name>.bin files (not the full output.bin), and this call downloads and
125
+ returns those instead of output.bin.
105
126
 
106
127
  Returns:
107
- Tuple of (E_field, converged):
108
- - E_field: Complex output field array. Vectorial: [3, nx, ny, nz], Scalar: [nx, ny, nz]
128
+ Tuple of (result, converged):
129
+ - result: WITHOUT monitors — the complex output field array (Vectorial:
130
+ [3, nx, ny, nz], Scalar: [nx, ny, nz]). WITH monitors — a LIST of monitor field
131
+ arrays in the SAME ORDER as the `monitors` argument (disabled monitors skipped),
132
+ each shaped per its own sub-cube.
109
133
  - converged: True if simulation completed successfully
110
134
  """
111
135
  # 1. Load credentials
@@ -221,7 +245,7 @@ def execute_via_api(
221
245
 
222
246
  if pre is not None:
223
247
  status = (pre.get("status") or "").lower()
224
- description = pre.get("description") or ""
248
+ pre_description = pre.get("description") or ""
225
249
  est = pre.get("estimate") or {}
226
250
  logger.info(
227
251
  f"Preflight: status={status} "
@@ -229,24 +253,24 @@ def execute_via_api(
229
253
  f"(E-field {est.get('efieldMemoryGB', 0):.2f} GB + workspace {est.get('workspaceMemoryGB', 0):.2f} GB) "
230
254
  f"grid={est.get('nx')}x{est.get('ny')}x{est.get('nz')}"
231
255
  )
232
- logger.info(f"Preflight: {description}")
256
+ logger.info(f"Preflight: {pre_description}")
233
257
  for s in pre.get("suggestedAdjustments") or []:
234
258
  logger.info(f" Suggestion: {s}")
235
259
  if status == "error":
236
260
  raise RuntimeError(
237
- f"Preflight rejected the simulation: {description} "
261
+ f"Preflight rejected the simulation: {pre_description} "
238
262
  f"(no upload was attempted). "
239
263
  f"Suggested adjustments: {pre.get('suggestedAdjustments') or '(none)'}"
240
264
  )
241
265
  if status == "warning":
242
- logger.warning(f"Preflight warning: {description}")
266
+ logger.warning(f"Preflight warning: {pre_description}")
243
267
 
244
268
  # 6. Create or reuse project/version
245
269
  auto_create = not project_id or not version_id
246
270
  if auto_create:
247
271
  logger.info("Creating project...")
248
272
  project_result = client.create_project(
249
- name=f"{description} - Wavesim Python API",
273
+ name=description,
250
274
  app="wavesim",
251
275
  description=f"Multi-source simulation ({len(sources_info)} source(s))",
252
276
  )
@@ -543,34 +567,105 @@ def execute_via_api(
543
567
 
544
568
  logger.info(f" Simulation completed in {total_time:.1f} seconds!")
545
569
 
546
- # 14. Download output field
570
+ # 14. Download results
547
571
  logger.info("Downloading results...")
548
572
  server_info = client.get_server_info()
549
573
  use_machine_service = server_info["uses_machine_service"]
550
574
 
551
- output_file_id = None
552
-
575
+ # Collect this command's result files as (filename, file_id) pairs from
576
+ # whichever store holds them (machine service vs cloud blob storage).
577
+ result_files = []
553
578
  if use_machine_service:
554
- files = client.list_command_files(command_id)
555
- logger.info(f" Found {len(files)} files from machine service")
556
- for f in files:
557
- fname = f.get("fileName") or f.get("originalFileName", "")
558
- if fname.lower() == "output.bin":
559
- output_file_id = f.get("blobName")
560
- break
579
+ for f in client.list_command_files(command_id):
580
+ name = f.get("fileName") or f.get("originalFileName") or ""
581
+ fid = f.get("blobName")
582
+ if name and fid:
583
+ result_files.append((name, fid))
584
+ logger.info(f" Found {len(result_files)} files from machine service")
561
585
  else:
562
586
  version_info = client.get_project_version(project_id, version_id)
563
- output_file_ids = version_info.get("outputFileIds", [])
564
- logger.info(f" Found {len(output_file_ids)} output file IDs from cloud storage")
565
- for fid in output_file_ids:
587
+ for fid in version_info.get("outputFileIds", []):
588
+ name = ""
566
589
  try:
567
590
  preview = client.get_file_preview(project_id, version_id, fid)
568
- fname = preview.get("fileName") or preview.get("originalFileName", "")
569
- if fname.lower() == "output.bin":
570
- output_file_id = fid # Use the original ID from outputFileIds
571
- break
591
+ name = preview.get("fileName") or preview.get("originalFileName") or ""
572
592
  except Exception:
573
- continue
593
+ pass
594
+ result_files.append((name, fid))
595
+ logger.info(f" Found {len(result_files)} output file IDs from cloud storage")
596
+
597
+ # ----- Monitor runs --------------------------------------------------------
598
+ # When monitors are defined, the server crops each monitor's sub-cube out of
599
+ # output.bin and uploads only the monitor_<name>.bin files — the full output.bin
600
+ # is intentionally NOT uploaded (it can be many GB; see RayfosMachineService
601
+ # CommandManager). So we must NOT require output.bin here. Just like the output.bin
602
+ # path, we stream each file to a temp dir, read it into numpy, and clean up; the
603
+ # return value is a LIST of monitor fields in the SAME ORDER as the `monitors`
604
+ # argument (disabled monitors skipped).
605
+ if monitors:
606
+ by_name = {n.lower(): fid for (n, fid) in result_files if n}
607
+
608
+ # (monitor name, expected filename) for each enabled monitor, in definition order.
609
+ ordered = [
610
+ (m.get("Name", ""), _monitor_output_filename(m.get("Name", "")))
611
+ for m in monitors if m.get("IsEnabled", True)
612
+ ]
613
+ have_exact = any(fn.lower() in by_name for _, fn in ordered)
614
+
615
+ # Download list as (display_name, file_id). Exact matches preserve definition
616
+ # order; the fallback (name-sanitisation mismatch) returns every monitor_*.bin
617
+ # the server produced, in the order the store listed them.
618
+ if have_exact:
619
+ to_fetch = []
620
+ for mname, fname in ordered:
621
+ fid = by_name.get(fname.lower())
622
+ if fid is None:
623
+ logger.warning(f"Monitor '{mname}': expected file {fname} not found on server; skipping.")
624
+ continue
625
+ to_fetch.append((fname, fid))
626
+ else:
627
+ to_fetch = [
628
+ (n, fid) for (n, fid) in result_files
629
+ if n.lower().startswith("monitor_") and n.lower().endswith(".bin")
630
+ ]
631
+ if to_fetch:
632
+ logger.warning("No exact monitor filename match; returning all monitor_*.bin files in server order.")
633
+
634
+ if not to_fetch:
635
+ available = ", ".join(n for n, _ in result_files) or "(none)"
636
+ raise RuntimeError(
637
+ "No monitor output files found for this command. Available files: " + available
638
+ )
639
+
640
+ tmp_dir = tempfile.mkdtemp(prefix="wavesimpro_mon_")
641
+ try:
642
+ monitor_fields = []
643
+ for name, fid in to_fetch:
644
+ dest = Path(tmp_dir) / name
645
+ logger.info(f" Downloading monitor file: {name}")
646
+ client.download_file(project_id, version_id, fid, str(dest), command_id=command_id)
647
+ # Monitor files carry a self-describing V2 header, so dims come from the file.
648
+ fld = read_binary_field(dest, None, "Complex64")
649
+ # Squeeze trailing size-1 dims, same convention as output.bin.
650
+ while fld.ndim > 1 and fld.shape[-1] == 1:
651
+ fld = fld[..., 0]
652
+ monitor_fields.append(fld)
653
+ logger.info(f" -> {fld.shape}")
654
+ finally:
655
+ shutil.rmtree(tmp_dir, ignore_errors=True)
656
+
657
+ converged = status_str == "Completed"
658
+ logger.info("=" * 60)
659
+ logger.info(f"Simulation completed successfully! Returning {len(monitor_fields)} monitor field(s).")
660
+ logger.info("=" * 60)
661
+ return monitor_fields, converged
662
+
663
+ # ----- Plain runs (no monitors): require output.bin ------------------------
664
+ output_file_id = None
665
+ for name, fid in result_files:
666
+ if name.lower() == "output.bin":
667
+ output_file_id = fid
668
+ break
574
669
 
575
670
  if not output_file_id:
576
671
  raise RuntimeError("output.bin not found in command files")
@@ -3,8 +3,6 @@ Helper functions to build WavesimProperties JSON structure
3
3
 
4
4
  This module provides Python functions to build the correct JSON structure
5
5
  that matches RayfosCommandModels.WavesimProperties C# class.
6
-
7
- Ported from examples/wavesim_json_helper.py with fixes for MATLAB API parity.
8
6
  """
9
7
 
10
8
  import json
@@ -125,7 +125,8 @@ def simulate(
125
125
  4. RAYFOS_API_KEY / RAYFOS_API_URL environment variables
126
126
 
127
127
  Returns:
128
- u: np.ndarray, output field (same shape as wavesim.simulate()).
128
+ u: output field. WITHOUT monitors — np.ndarray (same shape as wavesim.simulate()).
129
+ WITH monitors — a list of monitor field arrays in the same order as `monitors`.
129
130
  iterations: int or None, last reported iteration count.
130
131
  residual_norm: float or None, last reported residual norm.
131
132
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavesimpro
3
- Version: 1.0.0
3
+ Version: 1.0.2
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