wavesimpro 0.9.1__tar.gz → 0.9.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: 0.9.1
3
+ Version: 0.9.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 = "0.9.1"
7
+ version = "0.9.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"
@@ -30,7 +30,7 @@ from .binary_utils import (
30
30
  )
31
31
  from .validate import validate_parameters, ParameterValidationError
32
32
  from .execute import execute_via_api
33
- from .simulate import simulate, configure
33
+ from .simulate import simulate, simulate_pro, configure
34
34
  from .json_helper import (
35
35
  create_simulation_domain,
36
36
  create_stl_primitive,
@@ -49,7 +49,7 @@ from .json_helper import (
49
49
  parse_progress_data,
50
50
  )
51
51
 
52
- __version__ = "0.9.0"
52
+ __version__ = "0.9.2"
53
53
  __all__ = [
54
54
  # Client
55
55
  "RayfosClient",
@@ -896,6 +896,7 @@ class RayfosClient:
896
896
  version_id: str,
897
897
  engine_preference: str = 'Any',
898
898
  use_gpu: Optional[bool] = None,
899
+ preferred_machine_id: Optional[str] = None,
899
900
  ) -> Dict[str, Any]:
900
901
  """
901
902
  Create a simulation command for a project version.
@@ -927,7 +928,7 @@ class RayfosClient:
927
928
  if self.config.machine_service_url:
928
929
  return self._create_command_machine_service(project_id, version_id)
929
930
  else:
930
- return self._create_command_cloud(project_id, version_id, engine_preference, use_gpu)
931
+ return self._create_command_cloud(project_id, version_id, engine_preference, use_gpu, preferred_machine_id)
931
932
 
932
933
  def _create_command_machine_service(
933
934
  self,
@@ -956,6 +957,7 @@ class RayfosClient:
956
957
  version_id: str,
957
958
  engine_preference: str = 'Any',
958
959
  use_gpu: Optional[bool] = None,
960
+ preferred_machine_id: Optional[str] = None,
959
961
  ) -> Dict[str, Any]:
960
962
  """Create command via RayfosJobServer (cloud mode)"""
961
963
  data: Dict[str, Any] = {}
@@ -963,6 +965,8 @@ class RayfosClient:
963
965
  data['enginePreference'] = engine_preference
964
966
  if use_gpu is not None:
965
967
  data['useGpu'] = use_gpu
968
+ if preferred_machine_id:
969
+ data['preferredMachineServiceId'] = preferred_machine_id
966
970
  return self._post(f"/api/projects/{project_id}/versions/{version_id}/command", json=data)
967
971
 
968
972
  # ========================================================================
@@ -63,6 +63,9 @@ def execute_via_api(
63
63
  progress_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
64
64
  engine_preference: str = "Any",
65
65
  use_gpu: Optional[bool] = None,
66
+ preferred_machine_id: Optional[str] = None,
67
+ alpha: float = 0.75,
68
+ keep_residuals_list: bool = False,
66
69
  ) -> Tuple[np.ndarray, bool]:
67
70
  """
68
71
  Execute a Wavesim simulation via the Rayfos REST API.
@@ -311,6 +314,8 @@ def execute_via_api(
311
314
  periodic_x=bool(periodic[0]),
312
315
  periodic_y=bool(periodic[1]),
313
316
  periodic_z=bool(periodic[2]),
317
+ alpha=float(alpha),
318
+ keep_residuals_list=bool(keep_residuals_list),
314
319
  )
315
320
 
316
321
  logger.info(f" Payload created with {len(sources_info)} source(s)")
@@ -321,7 +326,7 @@ def execute_via_api(
321
326
 
322
327
  # 12. Submit command
323
328
  logger.info("Submitting simulation command...")
324
- cmd_status = client.create_command(project_id, version_id, engine_preference, use_gpu)
329
+ cmd_status = client.create_command(project_id, version_id, engine_preference, use_gpu, preferred_machine_id)
325
330
  command_id = cmd_status.get("commandId") or cmd_status.get("id")
326
331
  if not command_id:
327
332
  raise RuntimeError("No command ID found in response")
@@ -342,6 +347,8 @@ def execute_via_api(
342
347
  print(f" Mode : {_mode}")
343
348
  print(f" Domain : {_domain_str} λ={wavelength} µm Δx={pixel_size} µm")
344
349
  print(f" Engine : {_engine} | GPU: {_gpu}")
350
+ if preferred_machine_id:
351
+ print(f" Machine : {preferred_machine_id}")
345
352
  print(f" Threshold : {threshold:.1e} max iter: {max_iterations}")
346
353
  print(f" {'─'*54}")
347
354
  start_time = time.time()
@@ -513,10 +520,10 @@ def execute_via_api(
513
520
 
514
521
  E = read_binary_field(output_data, dims, "Complex64")
515
522
 
516
- # Squeeze trailing size-1 spatial dimensions added for 1D/2D inputs
517
- for _ in range(3 - _input_ndim):
518
- if E.shape[-1] == 1:
519
- E = E[..., 0]
523
+ # Squeeze all trailing size-1 spatial dimensions to match local wavesim.simulate() output.
524
+ # Local always squeezes: [nx,1,1]→(nx,), [3,nx,1,1]→[3,nx], etc.
525
+ while E.ndim > 1 and E.shape[-1] == 1:
526
+ E = E[..., 0]
520
527
 
521
528
  logger.info(f" Output shape: {E.shape}")
522
529
 
@@ -640,7 +640,9 @@ def create_wavesim_properties(
640
640
  residual_norm_max_iterations: int = 10000,
641
641
  periodic_x: bool = False,
642
642
  periodic_y: bool = False,
643
- periodic_z: bool = False
643
+ periodic_z: bool = False,
644
+ alpha: float = 0.75,
645
+ keep_residuals_list: bool = False,
644
646
  ) -> Dict[str, Any]:
645
647
  """
646
648
  Create complete WavesimProperties structure
@@ -688,6 +690,8 @@ def create_wavesim_properties(
688
690
  "PeriodicX": periodic_x,
689
691
  "PeriodicY": periodic_y,
690
692
  "PeriodicZ": periodic_z,
693
+ "Alpha": alpha,
694
+ "KeepResidualsList": keep_residuals_list,
691
695
  "DefaultLengthUnit": "Micrometers",
692
696
  "CoordinateSystemCenter": {
693
697
  "X": {"Value": 0.0, "Unit": "Micrometers"},
@@ -719,6 +723,11 @@ def parse_progress_data(progress_obj: Optional[Dict[str, Any]]) -> Dict[str, Any
719
723
  if "status" in norm:
720
724
  info["status"] = norm["status"]
721
725
 
726
+ # Full residuals list (ProgressSnapshot.ResidualsList from machine service)
727
+ residuals_list = norm.get("residualslist") or norm.get("residuals_list")
728
+ if residuals_list and isinstance(residuals_list, list):
729
+ info["residuals_list"] = residuals_list
730
+
722
731
  progress_str = norm.get("progress")
723
732
  if progress_str and isinstance(progress_str, str):
724
733
  # Try JSON first
@@ -731,7 +740,15 @@ def parse_progress_data(progress_obj: Optional[Dict[str, Any]]) -> Dict[str, Any
731
740
  if "max_iterations" in progress_data:
732
741
  info["max_iterations"] = progress_data["max_iterations"]
733
742
  except (json.JSONDecodeError, TypeError, ValueError):
734
- # Parse human-readable summary text
743
+ # Intermediate C++ format: "C++ iteration 400, residual: 1.23E-04"
744
+ m = re.search(r'iteration\s+(\d+)', progress_str, re.IGNORECASE)
745
+ if m:
746
+ info["iteration"] = int(m.group(1))
747
+ m = re.search(r'residual[:\s]+([\d.E+\-]+)', progress_str, re.IGNORECASE)
748
+ if m:
749
+ info["residual_norm"] = float(m.group(1))
750
+
751
+ # Final completion summary: "Last Iteration : 200 / 10000"
735
752
  m = re.search(r'Last Iteration\s*:\s*(\d+)\s*/\s*(\d+)', progress_str)
736
753
  if m:
737
754
  info["iteration"] = int(m.group(1))
@@ -752,7 +769,14 @@ def parse_progress_data(progress_obj: Optional[Dict[str, Any]]) -> Dict[str, Any
752
769
  info["residual_norm"] = data_obj["residual_norm"]
753
770
  if "residual_norm_threshold" in data_obj:
754
771
  info["threshold"] = data_obj["residual_norm_threshold"]
772
+ rlist = data_obj.get("ResidualsList") or data_obj.get("residuals_list")
773
+ if rlist and isinstance(rlist, list):
774
+ info["residuals_list"] = rlist
755
775
  except (json.JSONDecodeError, TypeError, ValueError):
756
776
  pass
757
777
 
778
+ # Derive percentage if not already set but iteration + max_iterations are known
779
+ if "percentage" not in info and info.get("iteration") and info.get("max_iterations"):
780
+ info["percentage"] = 100.0 * info["iteration"] / info["max_iterations"]
781
+
758
782
  return info
@@ -11,17 +11,27 @@ Credentials are resolved automatically — run the setup wizard once on each mac
11
11
  Call configure() to override programmatically.
12
12
  """
13
13
 
14
+ import inspect
15
+ from pathlib import Path
14
16
  from typing import Callable, Optional, Sequence
15
17
 
16
18
  import numpy as np
17
19
 
18
20
  from .execute import execute_via_api
19
21
 
22
+
23
+ def _caller_script_name() -> str:
24
+ for frame_info in inspect.stack():
25
+ path = frame_info.filename
26
+ if "wavesimpro" not in path and path not in ("<stdin>", "<string>"):
27
+ return Path(path).stem
28
+ return "wavesimpro"
29
+
20
30
  # Module-level defaults — overridden by configure() or per-call kwargs
21
31
  _defaults = {
22
32
  "api_key": None,
23
33
  "api_url": None,
24
- "description": "Wavesim Python API",
34
+ "description": None,
25
35
  "poll_interval": 10.0,
26
36
  "engine_preference": "Any",
27
37
  "use_gpu": None,
@@ -75,8 +85,8 @@ def simulate(
75
85
  n_domains=None, # local only — accepted and ignored
76
86
  max_iterations: int = 100000,
77
87
  threshold: float = 1.0e-6,
78
- alpha: float = 0.75, # local only — accepted and ignored
79
- full_residuals: bool = False, # local only — accepted and ignored
88
+ alpha: float = 0.75,
89
+ full_residuals: bool = False,
80
90
  crop_boundaries: bool = True, # local only — accepted and ignored
81
91
  callback: Optional[Callable] = None,
82
92
  **kwargs,
@@ -108,7 +118,7 @@ def simulate(
108
118
  "Run the setup wizard once to get started:\n\n"
109
119
  " python -m wavesimpro setup\n"
110
120
  )
111
- description = _defaults["description"]
121
+ description = _defaults["description"] or _caller_script_name()
112
122
  poll_interval = _defaults["poll_interval"]
113
123
  engine_preference = _defaults["engine_preference"]
114
124
  effective_use_gpu = use_gpu if use_gpu is not None else _defaults["use_gpu"]
@@ -128,11 +138,13 @@ def simulate(
128
138
  src_field = src_array.astype(np.complex64)
129
139
  api_sources.append({"Es": src_field, "position": api_pos})
130
140
 
131
- _last = {"iteration": None, "residual_norm": None}
141
+ _last = {"iteration": None, "residual_norm": None, "residuals_list": None}
132
142
 
133
143
  def _progress_cb(info: dict):
134
144
  _last["iteration"] = info.get("iteration", _last["iteration"])
135
145
  _last["residual_norm"] = info.get("residual_norm", _last["residual_norm"])
146
+ if info.get("residuals_list") is not None:
147
+ _last["residuals_list"] = info["residuals_list"]
136
148
  if callback is not None and _last["iteration"] is not None:
137
149
  extra = {k: v for k, v in info.items() if k not in ("iteration", "residual_norm")}
138
150
  callback(None, _last["iteration"], None, _last["residual_norm"], **extra)
@@ -154,10 +166,121 @@ def simulate(
154
166
  poll_interval=poll_interval,
155
167
  engine_preference=engine_preference,
156
168
  use_gpu=effective_use_gpu,
169
+ alpha=alpha,
170
+ keep_residuals_list=full_residuals,
171
+ progress_callback=_progress_cb,
172
+ )
173
+
174
+ iterations = _last["iteration"] or 1
175
+ final_residual = _last["residual_norm"] if _last["residual_norm"] is not None else float("nan")
176
+ if full_residuals:
177
+ residual_norm = _last["residuals_list"] if _last["residuals_list"] else [final_residual] * iterations
178
+ else:
179
+ residual_norm = final_residual
180
+ return E, iterations, residual_norm
181
+
182
+
183
+ def simulate_pro(
184
+ permittivity: np.ndarray,
185
+ sources: Sequence[tuple],
186
+ wavelength: float,
187
+ pixel_size: float,
188
+ boundary_width: float = 1.0,
189
+ periodic: tuple = (False, False, False),
190
+ max_iterations: int = 100000,
191
+ threshold: float = 1.0e-6,
192
+ alpha: float = 0.75,
193
+ n_domains=None,
194
+ full_residuals: bool = False,
195
+ crop_boundaries: bool = True, # local only — accepted and ignored
196
+ callback: Optional[Callable] = None,
197
+ # remote execution options
198
+ engine: str = "Any",
199
+ use_gpu: Optional[bool] = None,
200
+ machine_id: Optional[str] = None,
201
+ poll_interval: float = 10.0,
202
+ description: Optional[str] = None,
203
+ server: Optional[str] = None,
204
+ **kwargs,
205
+ ):
206
+ """Execute a Wavesim simulation remotely with full control over execution options.
207
+
208
+ Drop-in compatible with wavesim.simulate() but exposes all remote-specific options
209
+ that simulate() manages via configure():
210
+
211
+ engine : 'Any' (default) | 'Cpp' | 'Python'
212
+ use_gpu : None (auto) | True (require GPU) | False (force CPU)
213
+ machine_id : preferred machine service ID to pin the job to a specific machine
214
+ poll_interval: seconds between status polls (default 10)
215
+ description : job label shown in the Rayfos dashboard (default: caller script name)
216
+ """
217
+ from .setup import resolve_credentials
218
+ _api_key, _api_url = resolve_credentials()
219
+ api_key = _defaults["api_key"] or _api_key
220
+ api_url = server or _defaults["api_url"] or _api_url
221
+ if not api_key:
222
+ raise SystemExit(
223
+ "\nwavesimpro: no API key configured.\n"
224
+ "Run the setup wizard once to get started:\n\n"
225
+ " python -m wavesimpro setup\n"
226
+ )
227
+
228
+ effective_description = description or _defaults["description"] or _caller_script_name()
229
+ effective_use_gpu = use_gpu if use_gpu is not None else _defaults["use_gpu"]
230
+
231
+ is_vectorial = any(len(s[1]) == 4 for s in sources)
232
+ boundary_width_px = int(np.round(boundary_width / pixel_size))
233
+
234
+ api_sources = []
235
+ for src_array, src_pos in sources:
236
+ pos = list(src_pos)
237
+ if is_vectorial:
238
+ pol_1based = int(pos[0]) + 1
239
+ api_pos = [pos[1] * pixel_size, pos[2] * pixel_size, pos[3] * pixel_size, pol_1based]
240
+ src_field = (src_array[0] if src_array.ndim == 4 else src_array).astype(np.complex64)
241
+ else:
242
+ api_pos = [pos[0] * pixel_size, pos[1] * pixel_size, pos[2] * pixel_size]
243
+ src_field = src_array.astype(np.complex64)
244
+ api_sources.append({"Es": src_field, "position": api_pos})
245
+
246
+ _last = {"iteration": None, "residual_norm": None, "residuals_list": None}
247
+
248
+ def _progress_cb(info: dict):
249
+ _last["iteration"] = info.get("iteration", _last["iteration"])
250
+ _last["residual_norm"] = info.get("residual_norm", _last["residual_norm"])
251
+ if info.get("residuals_list") is not None:
252
+ _last["residuals_list"] = info["residuals_list"]
253
+ if callback is not None and _last["iteration"] is not None:
254
+ extra = {k: v for k, v in info.items() if k not in ("iteration", "residual_norm")}
255
+ callback(None, _last["iteration"], None, _last["residual_norm"], **extra)
256
+
257
+ E, _ = execute_via_api(
258
+ refractive_index=permittivity,
259
+ source=api_sources,
260
+ wavelength=wavelength,
261
+ pixel_size=pixel_size,
262
+ boundary_width=boundary_width_px,
263
+ is_vectorial=is_vectorial,
264
+ data_type="Permittivity",
265
+ max_iterations=max_iterations,
266
+ threshold=threshold,
267
+ periodic=periodic,
268
+ api_key=api_key,
269
+ api_url=api_url,
270
+ description=effective_description,
271
+ poll_interval=poll_interval,
272
+ engine_preference=engine,
273
+ use_gpu=effective_use_gpu,
274
+ preferred_machine_id=machine_id,
275
+ alpha=alpha,
276
+ keep_residuals_list=full_residuals,
157
277
  progress_callback=_progress_cb,
158
278
  )
159
279
 
160
280
  iterations = _last["iteration"] or 1
161
281
  final_residual = _last["residual_norm"] if _last["residual_norm"] is not None else float("nan")
162
- residual_norm = [final_residual] * iterations if full_residuals else final_residual
282
+ if full_residuals:
283
+ residual_norm = _last["residuals_list"] if _last["residuals_list"] else [final_residual] * iterations
284
+ else:
285
+ residual_norm = final_residual
163
286
  return E, iterations, residual_norm
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavesimpro
3
- Version: 0.9.1
3
+ Version: 0.9.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