wavesimpro 0.9.0__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.
Files changed (21) hide show
  1. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/PKG-INFO +1 -1
  2. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/pyproject.toml +1 -1
  3. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/__init__.py +2 -2
  4. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/client.py +5 -1
  5. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/execute.py +98 -23
  6. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/json_helper.py +57 -30
  7. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/setup.py +12 -11
  8. wavesimpro-0.9.2/src/wavesimpro/simulate.py +286 -0
  9. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/validate.py +6 -4
  10. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro.egg-info/PKG-INFO +1 -1
  11. wavesimpro-0.9.0/src/wavesimpro/simulate.py +0 -162
  12. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/setup.cfg +0 -0
  13. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/__main__.py +0 -0
  14. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/binary_utils.py +0 -0
  15. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/config.py +0 -0
  16. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/exceptions.py +0 -0
  17. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/py.typed +0 -0
  18. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro.egg-info/SOURCES.txt +0 -0
  19. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
  20. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro.egg-info/requires.txt +0 -0
  21. {wavesimpro-0.9.0 → wavesimpro-0.9.2}/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.9.0
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.0"
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.
@@ -123,8 +126,13 @@ def execute_via_api(
123
126
  logger.info(f" API URL: {api_url}")
124
127
  logger.info(f" Engine: {engine_preference} GPU: {'auto' if use_gpu is None else ('GPU' if use_gpu else 'CPU')}")
125
128
 
126
- # 2. Extract domain info
127
- nx, ny, nz = refractive_index.shape[:3]
129
+ # 2. Extract domain info — pad to 3D for 1D/2D inputs
130
+ _input_ndim = refractive_index.ndim
131
+ _ri_shape = refractive_index.shape
132
+ while len(_ri_shape) < 3:
133
+ _ri_shape = _ri_shape + (1,)
134
+ refractive_index = refractive_index.reshape(_ri_shape)
135
+ nx, ny, nz = _ri_shape[0], _ri_shape[1], _ri_shape[2]
128
136
 
129
137
  logger.info(f" Domain: [{nx}, {ny}, {nz}]")
130
138
  logger.info(f" Wavelength: {wavelength:.3f} um")
@@ -137,7 +145,10 @@ def execute_via_api(
137
145
  if data_type == "RefractiveIndex":
138
146
  background_ri = float(np.min(np.real(refractive_index)))
139
147
  else:
140
- background_ri = float(np.sqrt(np.min(np.real(refractive_index))))
148
+ eps_real = np.real(refractive_index)
149
+ positive_eps = eps_real[eps_real > 0]
150
+ min_positive_eps = float(np.min(positive_eps)) if positive_eps.size > 0 else 1.0
151
+ background_ri = float(np.sqrt(min_positive_eps))
141
152
  em_data = refractive_index
142
153
 
143
154
  # 4. Prepare sources
@@ -303,6 +314,8 @@ def execute_via_api(
303
314
  periodic_x=bool(periodic[0]),
304
315
  periodic_y=bool(periodic[1]),
305
316
  periodic_z=bool(periodic[2]),
317
+ alpha=float(alpha),
318
+ keep_residuals_list=bool(keep_residuals_list),
306
319
  )
307
320
 
308
321
  logger.info(f" Payload created with {len(sources_info)} source(s)")
@@ -313,7 +326,7 @@ def execute_via_api(
313
326
 
314
327
  # 12. Submit command
315
328
  logger.info("Submitting simulation command...")
316
- 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)
317
330
  command_id = cmd_status.get("commandId") or cmd_status.get("id")
318
331
  if not command_id:
319
332
  raise RuntimeError("No command ID found in response")
@@ -321,11 +334,32 @@ def execute_via_api(
321
334
 
322
335
  # 13. Poll for completion
323
336
  logger.info("Waiting for simulation to complete...")
337
+ _mode = "Maxwell (vectorial)" if is_vectorial else "Helmholtz (scalar)"
338
+ _engine = engine_preference if engine_preference and engine_preference != "Any" else "Any (auto-select)"
339
+ _gpu = "auto" if use_gpu is None else ("GPU" if use_gpu else "CPU")
340
+ _domain_str = f"{nx}×{ny}×{nz} voxels"
341
+ print(f"\n {'─'*54}")
342
+ print(f" Rayfos remote simulation")
343
+ print(f" {'─'*54}")
344
+ print(f" Project : {project_id}")
345
+ print(f" Version : {version_id}")
346
+ print(f" Command : {command_id}")
347
+ print(f" Mode : {_mode}")
348
+ print(f" Domain : {_domain_str} λ={wavelength} µm Δx={pixel_size} µm")
349
+ print(f" Engine : {_engine} | GPU: {_gpu}")
350
+ if preferred_machine_id:
351
+ print(f" Machine : {preferred_machine_id}")
352
+ print(f" Threshold : {threshold:.1e} max iter: {max_iterations}")
353
+ print(f" {'─'*54}")
324
354
  start_time = time.time()
325
355
  consecutive_errors = 0
326
356
  max_consecutive_errors = 3
327
357
  final_status = None
328
358
 
359
+ # C# enum: 7=Cancelled, 9=Running, 10=Completed, 11=Failed, 12=FailedRemovedFromMachine
360
+ _TERMINAL_INT = {7, 8, 10, 11, 12}
361
+ _TERMINAL_STR = {"Completed", "Failed", "Cancelled", "Deleted", "FailedRemovedFromMachine"}
362
+
329
363
  while True:
330
364
  try:
331
365
  status = client.get_command_status(command_id)
@@ -345,28 +379,63 @@ def execute_via_api(
345
379
  continue
346
380
 
347
381
  elapsed = time.time() - start_time
348
- status_str = status.get("status", "Unknown")
349
382
 
350
- # Parse progress if available
351
- progress_dto = status.get("progressDto")
383
+ # Resolve status string — API may return 'status' (str) or 'commandStatus' (int)
384
+ raw_status = status.get("status") or status.get("Status")
385
+ cmd_status_int = status.get("commandStatus") or status.get("CommandStatus")
386
+ if raw_status:
387
+ status_str = raw_status
388
+ elif cmd_status_int is not None:
389
+ status_str = RayfosClient._parse_command_status(cmd_status_int)
390
+ else:
391
+ status_str = "Unknown"
392
+
393
+ # Find progress dto — try multiple field names
394
+ progress_dto = (
395
+ status.get("progress")
396
+ or status.get("Progress")
397
+ or status.get("progressDto")
398
+ or status.get("CommandProgress")
399
+ )
400
+
352
401
  if progress_dto:
353
402
  progress_info = parse_progress_data(progress_dto)
354
403
  if progress_callback:
355
404
  progress_callback(progress_info)
356
-
357
- residual = progress_info.get("residual_norm")
358
- iteration = progress_info.get("iteration")
359
- if residual is not None:
360
- logger.info(f" [{elapsed:.0f}s] {status_str} (residual: {residual:.2e}, iter: {iteration})")
361
- elif iteration is not None:
362
- max_iter = progress_info.get("max_iterations", "?")
363
- logger.info(f" [{elapsed:.0f}s] {status_str} (iter {iteration}/{max_iter})")
364
- else:
365
- logger.info(f" [{elapsed:.0f}s] {status_str}")
366
405
  else:
367
- logger.info(f" [{elapsed:.0f}s] {status_str}")
368
-
369
- if status_str in ("Completed", "Failed", "Cancelled"):
406
+ progress_info = {}
407
+
408
+ # Top-level CommandStatusDto fields
409
+ pct = progress_info.get("percentage") or status.get("progressValue") or status.get("ProgressValue")
410
+ status_text = status.get("statusText") or status.get("StatusText")
411
+ machine_id = status.get("preferredMachineServiceId") or status.get("PreferredMachineServiceId")
412
+
413
+ residual = progress_info.get("residual_norm")
414
+ iteration = progress_info.get("iteration")
415
+ max_iter = progress_info.get("max_iterations")
416
+
417
+ details = []
418
+ if pct is not None and float(pct) > 0:
419
+ details.append(f"{float(pct):.0f}%")
420
+ if iteration is not None:
421
+ details.append(f"iter {iteration}" + (f"/{max_iter}" if max_iter else ""))
422
+ if residual is not None:
423
+ details.append(f"residual {residual:.2e}")
424
+ if machine_id:
425
+ details.append(f"machine {machine_id}")
426
+ if status_text and status_text.strip() and status_text.strip().lower() != status_str.lower():
427
+ details.append(status_text.strip())
428
+
429
+ detail_str = ", ".join(details)
430
+ msg = f" [{elapsed:.0f}s] {status_str}" + (f" — {detail_str}" if detail_str else "")
431
+ logger.info(msg)
432
+ print(msg)
433
+
434
+ is_terminal = (
435
+ status_str in _TERMINAL_STR
436
+ or (cmd_status_int is not None and cmd_status_int in _TERMINAL_INT)
437
+ )
438
+ if is_terminal:
370
439
  final_status = status
371
440
  break
372
441
 
@@ -374,9 +443,9 @@ def execute_via_api(
374
443
 
375
444
  total_time = time.time() - start_time
376
445
 
377
- if final_status.get("status") != "Completed":
446
+ if status_str != "Completed":
378
447
  # Build detailed error message
379
- error_parts = [f"Simulation ended with status: {final_status.get('status')}"]
448
+ error_parts = [f"Simulation ended with status: {status_str}"]
380
449
  for field in ("errorMessage", "ErrorMessage", "statusText", "StatusText"):
381
450
  val = final_status.get(field)
382
451
  if val:
@@ -450,9 +519,15 @@ def execute_via_api(
450
519
  dims = (nx, ny, nz)
451
520
 
452
521
  E = read_binary_field(output_data, dims, "Complex64")
522
+
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]
527
+
453
528
  logger.info(f" Output shape: {E.shape}")
454
529
 
455
- converged = final_status.get("status") == "Completed"
530
+ converged = status_str == "Completed"
456
531
 
457
532
  logger.info("=" * 60)
458
533
  logger.info("Simulation completed successfully!")
@@ -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"},
@@ -699,33 +703,36 @@ def create_wavesim_properties(
699
703
 
700
704
  def parse_progress_data(progress_obj: Optional[Dict[str, Any]]) -> Dict[str, Any]:
701
705
  """
702
- Parse CommandProgressDto with nested JSON strings.
703
-
704
- Args:
705
- progress_obj: CommandProgressDto dict from get_command_status()
706
+ Parse the progress dict from get_command_status().
706
707
 
707
- Returns:
708
- Dict with parsed progress fields: status, iteration, percentage,
709
- max_iterations, residual_norm, threshold
708
+ Handles both JSON-structured progress and the human-readable text format
709
+ the server embeds in the final Completed status, e.g.:
710
+ Last Iteration : 200 / 10000
711
+ Final Residual : 9.406141E-008
710
712
  """
713
+ import re
714
+
711
715
  info: Dict[str, Any] = {}
712
716
 
713
717
  if not progress_obj:
714
718
  return info
715
719
 
716
- if "status" in progress_obj:
717
- info["status"] = progress_obj["status"]
720
+ # Normalise keys to lowercase so we handle both 'Status'/'status', 'Progress'/'progress', etc.
721
+ norm = {k.lower(): v for k, v in progress_obj.items()}
718
722
 
719
- # Parse Progress JSON string
720
- progress_str = progress_obj.get("progress")
721
- if progress_str:
722
- try:
723
- if isinstance(progress_str, str):
724
- import json as _json
725
- progress_data = _json.loads(progress_str)
726
- else:
727
- progress_data = progress_str
723
+ if "status" in norm:
724
+ info["status"] = norm["status"]
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
728
730
 
731
+ progress_str = norm.get("progress")
732
+ if progress_str and isinstance(progress_str, str):
733
+ # Try JSON first
734
+ try:
735
+ progress_data = json.loads(progress_str)
729
736
  if "iteration" in progress_data:
730
737
  info["iteration"] = progress_data["iteration"]
731
738
  if "percentage" in progress_data:
@@ -733,23 +740,43 @@ def parse_progress_data(progress_obj: Optional[Dict[str, Any]]) -> Dict[str, Any
733
740
  if "max_iterations" in progress_data:
734
741
  info["max_iterations"] = progress_data["max_iterations"]
735
742
  except (json.JSONDecodeError, TypeError, ValueError):
736
- pass
737
-
738
- # Parse Data JSON string
739
- data_str = progress_obj.get("data")
740
- if data_str:
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"
752
+ m = re.search(r'Last Iteration\s*:\s*(\d+)\s*/\s*(\d+)', progress_str)
753
+ if m:
754
+ info["iteration"] = int(m.group(1))
755
+ info["max_iterations"] = int(m.group(2))
756
+ m = re.search(r'Final Residual\s*:\s*([\d.E+\-]+)', progress_str, re.IGNORECASE)
757
+ if m:
758
+ info["residual_norm"] = float(m.group(1))
759
+ m = re.search(r'Time/iter\s*:\s*([\d.]+)\s*ms', progress_str)
760
+ if m:
761
+ info["time_per_iter_ms"] = float(m.group(1))
762
+
763
+ # Parse data field if present (JSON dict or string)
764
+ data_val = norm.get("data")
765
+ if data_val:
741
766
  try:
742
- if isinstance(data_str, str):
743
- import json as _json
744
- data_obj = _json.loads(data_str)
745
- else:
746
- data_obj = data_str
747
-
767
+ data_obj = json.loads(data_val) if isinstance(data_val, str) else data_val
748
768
  if "residual_norm" in data_obj:
749
769
  info["residual_norm"] = data_obj["residual_norm"]
750
770
  if "residual_norm_threshold" in data_obj:
751
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
752
775
  except (json.JSONDecodeError, TypeError, ValueError):
753
776
  pass
754
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
+
755
782
  return info
@@ -1,11 +1,11 @@
1
1
  """
2
- Setup wizard and credential management for wavesimPro_api.
2
+ Setup wizard and credential management for wavesimpro.
3
3
 
4
- Global config lives at ~/.wavesimPro_api/.env and is written by run_setup().
4
+ Global config lives at ~/.wavesimpro/.env and is written by run_setup().
5
5
  Credential resolution order:
6
6
  1. configure() call
7
7
  2. Local .env in the current working directory
8
- 3. ~/.wavesimPro_api/.env (global, written by this wizard)
8
+ 3. ~/.wavesimpro/.env (global, written by this wizard)
9
9
  4. RAYFOS_API_KEY / RAYFOS_API_URL environment variables
10
10
  """
11
11
 
@@ -14,7 +14,7 @@ import os
14
14
  from pathlib import Path
15
15
 
16
16
  DEFAULT_API_URL = "https://server.rayfos.com"
17
- GLOBAL_CONFIG_DIR = Path.home() / ".wavesimPro_api"
17
+ GLOBAL_CONFIG_DIR = Path.home() / ".wavesimpro"
18
18
  GLOBAL_CONFIG_FILE = GLOBAL_CONFIG_DIR / ".env"
19
19
 
20
20
 
@@ -41,7 +41,7 @@ def save_global_config(api_key: str, api_url: str):
41
41
 
42
42
  def resolve_credentials() -> tuple[str | None, str]:
43
43
  """Return (api_key, api_url) from the first source that has them."""
44
- from . import _defaults # module-level configure() values
44
+ from .simulate import _defaults
45
45
 
46
46
  api_key = _defaults.get("api_key")
47
47
  api_url = _defaults.get("api_url")
@@ -88,7 +88,7 @@ def _test_connection(api_key: str, api_url: str) -> bool:
88
88
 
89
89
  def run_setup():
90
90
  print()
91
- print("wavesimPro_api — setup wizard")
91
+ print("wavesimpro — setup wizard")
92
92
  print("=" * 40)
93
93
 
94
94
  existing = load_global_config()
@@ -107,10 +107,12 @@ def run_setup():
107
107
  return
108
108
 
109
109
  print()
110
- api_key = getpass.getpass("API key (input hidden): ").strip()
110
+ api_key = input("API key: ").strip()
111
111
  if not api_key:
112
112
  print("No API key entered. Aborting.")
113
113
  return
114
+ masked_preview = api_key[:6] + "*" * max(0, len(api_key) - 6)
115
+ print(f" Got: {masked_preview}")
114
116
 
115
117
  url_prompt = f"Server URL [{DEFAULT_API_URL}]: "
116
118
  api_url = input(url_prompt).strip() or DEFAULT_API_URL
@@ -126,27 +128,26 @@ def run_setup():
126
128
  save_global_config(api_key, api_url)
127
129
  print()
128
130
  print(f"Config saved to: {GLOBAL_CONFIG_FILE}")
129
- print("You're all set. Import wavesimPro_api in any project without extra setup.")
131
+ print("You're all set. Import wavesimpro in any project without extra setup.")
130
132
  print()
131
133
 
132
134
 
133
135
  def run_status():
134
136
  print()
135
- print("wavesimPro_api — current configuration")
137
+ print("wavesimpro — current configuration")
136
138
  print("=" * 40)
137
139
 
138
140
  api_key, api_url = resolve_credentials()
139
141
 
140
142
  if not api_key:
141
143
  print("No credentials found.")
142
- print(f"Run: python -m wavesimPro_api setup")
144
+ print("Run: python -m wavesimpro setup")
143
145
  return
144
146
 
145
147
  masked = api_key[:6] + "..." + api_key[-4:] if len(api_key) > 10 else "***"
146
148
  print(f"API key : {masked}")
147
149
  print(f"Server : {api_url}")
148
150
 
149
- # Show source
150
151
  cfg = load_global_config()
151
152
  if cfg.get("RAYFOS_API_KEY") == api_key:
152
153
  print(f"Source : {GLOBAL_CONFIG_FILE}")
@@ -0,0 +1,286 @@
1
+ """
2
+ Remote simulate — drop-in replacement for wavesim.simulate() that executes via the Rayfos API.
3
+
4
+ To switch an example from local to remote, change only the import:
5
+ from wavesim.simulate import simulate # local GPU
6
+ from wavesimPro_api import simulate # Rayfos cloud
7
+
8
+ Credentials are resolved automatically — run the setup wizard once on each machine:
9
+ python -m wavesimPro_api setup
10
+
11
+ Call configure() to override programmatically.
12
+ """
13
+
14
+ import inspect
15
+ from pathlib import Path
16
+ from typing import Callable, Optional, Sequence
17
+
18
+ import numpy as np
19
+
20
+ from .execute import execute_via_api
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
+
30
+ # Module-level defaults — overridden by configure() or per-call kwargs
31
+ _defaults = {
32
+ "api_key": None,
33
+ "api_url": None,
34
+ "description": None,
35
+ "poll_interval": 10.0,
36
+ "engine_preference": "Any",
37
+ "use_gpu": None,
38
+ }
39
+
40
+
41
+ def configure(
42
+ *,
43
+ api_key: Optional[str] = None,
44
+ api_url: Optional[str] = None,
45
+ description: Optional[str] = None,
46
+ poll_interval: Optional[float] = None,
47
+ engine_preference: Optional[str] = None,
48
+ use_gpu: Optional[bool] = None,
49
+ ):
50
+ """Set module-level defaults for remote execution.
51
+
52
+ Call once at the top of a script to avoid repeating credentials on every
53
+ simulate() call. Any value left as None keeps its current default.
54
+
55
+ Args:
56
+ api_key: Rayfos API key (overrides RAYFOS_API_KEY env var).
57
+ api_url: Rayfos API base URL (overrides RAYFOS_API_URL env var).
58
+ description: Job label shown in the Rayfos dashboard.
59
+ poll_interval: Status polling interval in seconds.
60
+ engine_preference: 'Any' (default), 'Python', or 'Cpp'.
61
+ use_gpu: None (auto), True (require GPU), False (force CPU).
62
+ """
63
+ if api_key is not None:
64
+ _defaults["api_key"] = api_key
65
+ if api_url is not None:
66
+ _defaults["api_url"] = api_url
67
+ if description is not None:
68
+ _defaults["description"] = description
69
+ if poll_interval is not None:
70
+ _defaults["poll_interval"] = poll_interval
71
+ if engine_preference is not None:
72
+ _defaults["engine_preference"] = engine_preference
73
+ if use_gpu is not None:
74
+ _defaults["use_gpu"] = use_gpu
75
+
76
+
77
+ def simulate(
78
+ permittivity: np.ndarray,
79
+ sources: Sequence[tuple],
80
+ wavelength: float,
81
+ pixel_size: float,
82
+ boundary_width: float = 1.0,
83
+ periodic: tuple = (False, False, False), # forwarded to PeriodicX/Y/Z in JSON
84
+ use_gpu=None, # forwarded to API (None = auto)
85
+ n_domains=None, # local only — accepted and ignored
86
+ max_iterations: int = 100000,
87
+ threshold: float = 1.0e-6,
88
+ alpha: float = 0.75,
89
+ full_residuals: bool = False,
90
+ crop_boundaries: bool = True, # local only — accepted and ignored
91
+ callback: Optional[Callable] = None,
92
+ **kwargs,
93
+ ):
94
+ """Execute a Wavesim simulation remotely via the Rayfos API.
95
+
96
+ Identical signature to wavesim.simulate() — swap the import, nothing else changes.
97
+ Local-only parameters (periodic, n_domains, alpha, full_residuals, crop_boundaries)
98
+ are silently ignored.
99
+
100
+ Credentials are resolved in this order:
101
+ 1. configure() call
102
+ 2. Local .env in the current working directory
103
+ 3. ~/.wavesimPro_api/.env (written by: python -m wavesimPro_api setup)
104
+ 4. RAYFOS_API_KEY / RAYFOS_API_URL environment variables
105
+
106
+ Returns:
107
+ u: np.ndarray, output field (same shape as wavesim.simulate()).
108
+ iterations: int or None, last reported iteration count.
109
+ residual_norm: float or None, last reported residual norm.
110
+ """
111
+ from .setup import resolve_credentials
112
+ _api_key, _api_url = resolve_credentials()
113
+ api_key = _defaults["api_key"] or _api_key
114
+ api_url = _defaults["api_url"] or _api_url
115
+ if not api_key:
116
+ raise SystemExit(
117
+ "\nwavesimpro: no API key configured.\n"
118
+ "Run the setup wizard once to get started:\n\n"
119
+ " python -m wavesimpro setup\n"
120
+ )
121
+ description = _defaults["description"] or _caller_script_name()
122
+ poll_interval = _defaults["poll_interval"]
123
+ engine_preference = _defaults["engine_preference"]
124
+ effective_use_gpu = use_gpu if use_gpu is not None else _defaults["use_gpu"]
125
+
126
+ is_vectorial = any(len(s[1]) == 4 for s in sources)
127
+ boundary_width_px = int(np.round(boundary_width / pixel_size))
128
+
129
+ api_sources = []
130
+ for src_array, src_pos in sources:
131
+ pos = list(src_pos)
132
+ if is_vectorial:
133
+ pol_1based = int(pos[0]) + 1
134
+ api_pos = [pos[1] * pixel_size, pos[2] * pixel_size, pos[3] * pixel_size, pol_1based]
135
+ src_field = (src_array[0] if src_array.ndim == 4 else src_array).astype(np.complex64)
136
+ else:
137
+ api_pos = [pos[0] * pixel_size, pos[1] * pixel_size, pos[2] * pixel_size]
138
+ src_field = src_array.astype(np.complex64)
139
+ api_sources.append({"Es": src_field, "position": api_pos})
140
+
141
+ _last = {"iteration": None, "residual_norm": None, "residuals_list": None}
142
+
143
+ def _progress_cb(info: dict):
144
+ _last["iteration"] = info.get("iteration", _last["iteration"])
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"]
148
+ if callback is not None and _last["iteration"] is not None:
149
+ extra = {k: v for k, v in info.items() if k not in ("iteration", "residual_norm")}
150
+ callback(None, _last["iteration"], None, _last["residual_norm"], **extra)
151
+
152
+ E, _ = execute_via_api(
153
+ refractive_index=permittivity,
154
+ source=api_sources,
155
+ wavelength=wavelength,
156
+ pixel_size=pixel_size,
157
+ boundary_width=boundary_width_px,
158
+ is_vectorial=is_vectorial,
159
+ data_type="Permittivity",
160
+ max_iterations=max_iterations,
161
+ threshold=threshold,
162
+ periodic=periodic,
163
+ api_key=api_key,
164
+ api_url=api_url,
165
+ description=description,
166
+ poll_interval=poll_interval,
167
+ engine_preference=engine_preference,
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,
277
+ progress_callback=_progress_cb,
278
+ )
279
+
280
+ iterations = _last["iteration"] or 1
281
+ final_residual = _last["residual_norm"] if _last["residual_norm"] is not None else float("nan")
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
286
+ return E, iterations, residual_norm
@@ -68,8 +68,10 @@ def validate_parameters(
68
68
  shape = shape + (1,)
69
69
  nx, ny, nz = shape[0], shape[1], shape[2]
70
70
 
71
- if nx < 2 or ny < 2 or nz < 2:
72
- errors.append(f"Domain too small: [{nx}, {ny}, {nz}]. Minimum is [2, 2, 2]")
71
+ # Allow size-1 in any dimension (collapsed 2D/1D), but non-singleton dims must be >= 2
72
+ _small = [d for d in (nx, ny, nz) if d != 1 and d < 2]
73
+ if _small or all(d == 1 for d in (nx, ny, nz)):
74
+ errors.append(f"Domain too small: [{nx}, {ny}, {nz}]. Non-singleton dimensions must be >= 2")
73
75
 
74
76
  if np.any(np.isnan(refractive_index)):
75
77
  errors.append("Refractive index contains NaN values")
@@ -151,8 +153,8 @@ def validate_parameters(
151
153
  exceeded.append(f"{dim_names[i]} ({dims[i]}->{max_allowed})")
152
154
  if exceeded:
153
155
  warnings.append(
154
- f"Boundary width ({boundary_width} pixels) exceeds maximum for "
155
- f"dimension(s): {', '.join(exceeded)}. Backend will clamp boundary width per dimension."
156
+ f"Boundary width ({boundary_width} pixels) is large relative to domain size "
157
+ f"in dimension(s): {', '.join(exceeded)}. Results may differ from local execution."
156
158
  )
157
159
 
158
160
  # 7. Validate GPU memory requirements
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavesimpro
3
- Version: 0.9.0
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
@@ -1,162 +0,0 @@
1
- """
2
- Remote simulate — drop-in replacement for wavesim.simulate() that executes via the Rayfos API.
3
-
4
- To switch an example from local to remote, change only the import:
5
- from wavesim.simulate import simulate # local GPU
6
- from wavesimPro_api import simulate # Rayfos cloud
7
-
8
- Credentials are resolved automatically — run the setup wizard once on each machine:
9
- python -m wavesimPro_api setup
10
-
11
- Call configure() to override programmatically.
12
- """
13
-
14
- from typing import Callable, Optional, Sequence
15
-
16
- import numpy as np
17
-
18
- from .execute import execute_via_api
19
-
20
- # Module-level defaults — overridden by configure() or per-call kwargs
21
- _defaults = {
22
- "api_key": None,
23
- "api_url": None,
24
- "description": "Wavesim Python API",
25
- "poll_interval": 10.0,
26
- "engine_preference": "Any",
27
- "use_gpu": None,
28
- }
29
-
30
-
31
- def configure(
32
- *,
33
- api_key: Optional[str] = None,
34
- api_url: Optional[str] = None,
35
- description: Optional[str] = None,
36
- poll_interval: Optional[float] = None,
37
- engine_preference: Optional[str] = None,
38
- use_gpu: Optional[bool] = None,
39
- ):
40
- """Set module-level defaults for remote execution.
41
-
42
- Call once at the top of a script to avoid repeating credentials on every
43
- simulate() call. Any value left as None keeps its current default.
44
-
45
- Args:
46
- api_key: Rayfos API key (overrides RAYFOS_API_KEY env var).
47
- api_url: Rayfos API base URL (overrides RAYFOS_API_URL env var).
48
- description: Job label shown in the Rayfos dashboard.
49
- poll_interval: Status polling interval in seconds.
50
- engine_preference: 'Any' (default), 'Python', or 'Cpp'.
51
- use_gpu: None (auto), True (require GPU), False (force CPU).
52
- """
53
- if api_key is not None:
54
- _defaults["api_key"] = api_key
55
- if api_url is not None:
56
- _defaults["api_url"] = api_url
57
- if description is not None:
58
- _defaults["description"] = description
59
- if poll_interval is not None:
60
- _defaults["poll_interval"] = poll_interval
61
- if engine_preference is not None:
62
- _defaults["engine_preference"] = engine_preference
63
- if use_gpu is not None:
64
- _defaults["use_gpu"] = use_gpu
65
-
66
-
67
- def simulate(
68
- permittivity: np.ndarray,
69
- sources: Sequence[tuple],
70
- wavelength: float,
71
- pixel_size: float,
72
- boundary_width: float = 1.0,
73
- periodic: tuple = (False, False, False), # forwarded to PeriodicX/Y/Z in JSON
74
- use_gpu=None, # forwarded to API (None = auto)
75
- n_domains=None, # local only — accepted and ignored
76
- max_iterations: int = 100000,
77
- 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
80
- crop_boundaries: bool = True, # local only — accepted and ignored
81
- callback: Optional[Callable] = None,
82
- **kwargs,
83
- ):
84
- """Execute a Wavesim simulation remotely via the Rayfos API.
85
-
86
- Identical signature to wavesim.simulate() — swap the import, nothing else changes.
87
- Local-only parameters (periodic, n_domains, alpha, full_residuals, crop_boundaries)
88
- are silently ignored.
89
-
90
- Credentials are resolved in this order:
91
- 1. configure() call
92
- 2. Local .env in the current working directory
93
- 3. ~/.wavesimPro_api/.env (written by: python -m wavesimPro_api setup)
94
- 4. RAYFOS_API_KEY / RAYFOS_API_URL environment variables
95
-
96
- Returns:
97
- u: np.ndarray, output field (same shape as wavesim.simulate()).
98
- iterations: int or None, last reported iteration count.
99
- residual_norm: float or None, last reported residual norm.
100
- """
101
- from .setup import resolve_credentials
102
- _api_key, _api_url = resolve_credentials()
103
- api_key = _defaults["api_key"] or _api_key
104
- api_url = _defaults["api_url"] or _api_url
105
- if not api_key:
106
- raise RuntimeError(
107
- "No API key found. Run the setup wizard once:\n"
108
- " python -m wavesimPro_api setup\n"
109
- "Or set RAYFOS_API_KEY in your environment / .env file."
110
- )
111
- description = _defaults["description"]
112
- poll_interval = _defaults["poll_interval"]
113
- engine_preference = _defaults["engine_preference"]
114
- effective_use_gpu = use_gpu if use_gpu is not None else _defaults["use_gpu"]
115
-
116
- is_vectorial = any(len(s[1]) == 4 for s in sources)
117
- boundary_width_px = int(np.round(boundary_width / pixel_size))
118
-
119
- api_sources = []
120
- for src_array, src_pos in sources:
121
- pos = list(src_pos)
122
- if is_vectorial:
123
- pol_1based = int(pos[0]) + 1
124
- api_pos = [pos[1] * pixel_size, pos[2] * pixel_size, pos[3] * pixel_size, pol_1based]
125
- src_field = (src_array[0] if src_array.ndim == 4 else src_array).astype(np.complex64)
126
- else:
127
- api_pos = [pos[0] * pixel_size, pos[1] * pixel_size, pos[2] * pixel_size]
128
- src_field = src_array.astype(np.complex64)
129
- api_sources.append({"Es": src_field, "position": api_pos})
130
-
131
- _last = {"iteration": None, "residual_norm": None}
132
-
133
- def _progress_cb(info: dict):
134
- _last["iteration"] = info.get("iteration", _last["iteration"])
135
- _last["residual_norm"] = info.get("residual_norm", _last["residual_norm"])
136
- if callback is not None:
137
- extra = {k: v for k, v in info.items() if k not in ("iteration", "residual_norm")}
138
- # Mirror the Richardson callback signature so existing callbacks work unchanged:
139
- # callback(domain, iteration, x, residual_norm, **kwargs)
140
- callback(None, _last["iteration"], None, _last["residual_norm"], **extra)
141
-
142
- E, _ = execute_via_api(
143
- refractive_index=permittivity,
144
- source=api_sources,
145
- wavelength=wavelength,
146
- pixel_size=pixel_size,
147
- boundary_width=boundary_width_px,
148
- is_vectorial=is_vectorial,
149
- data_type="Permittivity",
150
- max_iterations=max_iterations,
151
- threshold=threshold,
152
- periodic=periodic,
153
- api_key=api_key,
154
- api_url=api_url,
155
- description=description,
156
- poll_interval=poll_interval,
157
- engine_preference=engine_preference,
158
- use_gpu=effective_use_gpu,
159
- progress_callback=_progress_cb,
160
- )
161
-
162
- return E, _last["iteration"], _last["residual_norm"]
File without changes