wavesimpro 0.9.0__tar.gz → 0.9.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.9.0 → wavesimpro-0.9.1}/PKG-INFO +1 -1
  2. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/pyproject.toml +1 -1
  3. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/execute.py +90 -22
  4. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/json_helper.py +32 -29
  5. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/setup.py +12 -11
  6. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/simulate.py +9 -8
  7. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/validate.py +6 -4
  8. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro.egg-info/PKG-INFO +1 -1
  9. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/setup.cfg +0 -0
  10. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/__init__.py +0 -0
  11. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/__main__.py +0 -0
  12. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/binary_utils.py +0 -0
  13. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/client.py +0 -0
  14. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/config.py +0 -0
  15. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/exceptions.py +0 -0
  16. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/py.typed +0 -0
  17. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro.egg-info/SOURCES.txt +0 -0
  18. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
  19. {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro.egg-info/requires.txt +0 -0
  20. {wavesimpro-0.9.0 → wavesimpro-0.9.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.9.0
3
+ Version: 0.9.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.9.0"
7
+ version = "0.9.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"
@@ -123,8 +123,13 @@ def execute_via_api(
123
123
  logger.info(f" API URL: {api_url}")
124
124
  logger.info(f" Engine: {engine_preference} GPU: {'auto' if use_gpu is None else ('GPU' if use_gpu else 'CPU')}")
125
125
 
126
- # 2. Extract domain info
127
- nx, ny, nz = refractive_index.shape[:3]
126
+ # 2. Extract domain info — pad to 3D for 1D/2D inputs
127
+ _input_ndim = refractive_index.ndim
128
+ _ri_shape = refractive_index.shape
129
+ while len(_ri_shape) < 3:
130
+ _ri_shape = _ri_shape + (1,)
131
+ refractive_index = refractive_index.reshape(_ri_shape)
132
+ nx, ny, nz = _ri_shape[0], _ri_shape[1], _ri_shape[2]
128
133
 
129
134
  logger.info(f" Domain: [{nx}, {ny}, {nz}]")
130
135
  logger.info(f" Wavelength: {wavelength:.3f} um")
@@ -137,7 +142,10 @@ def execute_via_api(
137
142
  if data_type == "RefractiveIndex":
138
143
  background_ri = float(np.min(np.real(refractive_index)))
139
144
  else:
140
- background_ri = float(np.sqrt(np.min(np.real(refractive_index))))
145
+ eps_real = np.real(refractive_index)
146
+ positive_eps = eps_real[eps_real > 0]
147
+ min_positive_eps = float(np.min(positive_eps)) if positive_eps.size > 0 else 1.0
148
+ background_ri = float(np.sqrt(min_positive_eps))
141
149
  em_data = refractive_index
142
150
 
143
151
  # 4. Prepare sources
@@ -321,11 +329,30 @@ def execute_via_api(
321
329
 
322
330
  # 13. Poll for completion
323
331
  logger.info("Waiting for simulation to complete...")
332
+ _mode = "Maxwell (vectorial)" if is_vectorial else "Helmholtz (scalar)"
333
+ _engine = engine_preference if engine_preference and engine_preference != "Any" else "Any (auto-select)"
334
+ _gpu = "auto" if use_gpu is None else ("GPU" if use_gpu else "CPU")
335
+ _domain_str = f"{nx}×{ny}×{nz} voxels"
336
+ print(f"\n {'─'*54}")
337
+ print(f" Rayfos remote simulation")
338
+ print(f" {'─'*54}")
339
+ print(f" Project : {project_id}")
340
+ print(f" Version : {version_id}")
341
+ print(f" Command : {command_id}")
342
+ print(f" Mode : {_mode}")
343
+ print(f" Domain : {_domain_str} λ={wavelength} µm Δx={pixel_size} µm")
344
+ print(f" Engine : {_engine} | GPU: {_gpu}")
345
+ print(f" Threshold : {threshold:.1e} max iter: {max_iterations}")
346
+ print(f" {'─'*54}")
324
347
  start_time = time.time()
325
348
  consecutive_errors = 0
326
349
  max_consecutive_errors = 3
327
350
  final_status = None
328
351
 
352
+ # C# enum: 7=Cancelled, 9=Running, 10=Completed, 11=Failed, 12=FailedRemovedFromMachine
353
+ _TERMINAL_INT = {7, 8, 10, 11, 12}
354
+ _TERMINAL_STR = {"Completed", "Failed", "Cancelled", "Deleted", "FailedRemovedFromMachine"}
355
+
329
356
  while True:
330
357
  try:
331
358
  status = client.get_command_status(command_id)
@@ -345,28 +372,63 @@ def execute_via_api(
345
372
  continue
346
373
 
347
374
  elapsed = time.time() - start_time
348
- status_str = status.get("status", "Unknown")
349
375
 
350
- # Parse progress if available
351
- progress_dto = status.get("progressDto")
376
+ # Resolve status string — API may return 'status' (str) or 'commandStatus' (int)
377
+ raw_status = status.get("status") or status.get("Status")
378
+ cmd_status_int = status.get("commandStatus") or status.get("CommandStatus")
379
+ if raw_status:
380
+ status_str = raw_status
381
+ elif cmd_status_int is not None:
382
+ status_str = RayfosClient._parse_command_status(cmd_status_int)
383
+ else:
384
+ status_str = "Unknown"
385
+
386
+ # Find progress dto — try multiple field names
387
+ progress_dto = (
388
+ status.get("progress")
389
+ or status.get("Progress")
390
+ or status.get("progressDto")
391
+ or status.get("CommandProgress")
392
+ )
393
+
352
394
  if progress_dto:
353
395
  progress_info = parse_progress_data(progress_dto)
354
396
  if progress_callback:
355
397
  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
398
  else:
367
- logger.info(f" [{elapsed:.0f}s] {status_str}")
368
-
369
- if status_str in ("Completed", "Failed", "Cancelled"):
399
+ progress_info = {}
400
+
401
+ # Top-level CommandStatusDto fields
402
+ pct = progress_info.get("percentage") or status.get("progressValue") or status.get("ProgressValue")
403
+ status_text = status.get("statusText") or status.get("StatusText")
404
+ machine_id = status.get("preferredMachineServiceId") or status.get("PreferredMachineServiceId")
405
+
406
+ residual = progress_info.get("residual_norm")
407
+ iteration = progress_info.get("iteration")
408
+ max_iter = progress_info.get("max_iterations")
409
+
410
+ details = []
411
+ if pct is not None and float(pct) > 0:
412
+ details.append(f"{float(pct):.0f}%")
413
+ if iteration is not None:
414
+ details.append(f"iter {iteration}" + (f"/{max_iter}" if max_iter else ""))
415
+ if residual is not None:
416
+ details.append(f"residual {residual:.2e}")
417
+ if machine_id:
418
+ details.append(f"machine {machine_id}")
419
+ if status_text and status_text.strip() and status_text.strip().lower() != status_str.lower():
420
+ details.append(status_text.strip())
421
+
422
+ detail_str = ", ".join(details)
423
+ msg = f" [{elapsed:.0f}s] {status_str}" + (f" — {detail_str}" if detail_str else "")
424
+ logger.info(msg)
425
+ print(msg)
426
+
427
+ is_terminal = (
428
+ status_str in _TERMINAL_STR
429
+ or (cmd_status_int is not None and cmd_status_int in _TERMINAL_INT)
430
+ )
431
+ if is_terminal:
370
432
  final_status = status
371
433
  break
372
434
 
@@ -374,9 +436,9 @@ def execute_via_api(
374
436
 
375
437
  total_time = time.time() - start_time
376
438
 
377
- if final_status.get("status") != "Completed":
439
+ if status_str != "Completed":
378
440
  # Build detailed error message
379
- error_parts = [f"Simulation ended with status: {final_status.get('status')}"]
441
+ error_parts = [f"Simulation ended with status: {status_str}"]
380
442
  for field in ("errorMessage", "ErrorMessage", "statusText", "StatusText"):
381
443
  val = final_status.get(field)
382
444
  if val:
@@ -450,9 +512,15 @@ def execute_via_api(
450
512
  dims = (nx, ny, nz)
451
513
 
452
514
  E = read_binary_field(output_data, dims, "Complex64")
515
+
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]
520
+
453
521
  logger.info(f" Output shape: {E.shape}")
454
522
 
455
- converged = final_status.get("status") == "Completed"
523
+ converged = status_str == "Completed"
456
524
 
457
525
  logger.info("=" * 60)
458
526
  logger.info("Simulation completed successfully!")
@@ -699,33 +699,31 @@ def create_wavesim_properties(
699
699
 
700
700
  def parse_progress_data(progress_obj: Optional[Dict[str, Any]]) -> Dict[str, Any]:
701
701
  """
702
- Parse CommandProgressDto with nested JSON strings.
702
+ Parse the progress dict from get_command_status().
703
703
 
704
- Args:
705
- progress_obj: CommandProgressDto dict from get_command_status()
706
-
707
- Returns:
708
- Dict with parsed progress fields: status, iteration, percentage,
709
- max_iterations, residual_norm, threshold
704
+ Handles both JSON-structured progress and the human-readable text format
705
+ the server embeds in the final Completed status, e.g.:
706
+ Last Iteration : 200 / 10000
707
+ Final Residual : 9.406141E-008
710
708
  """
709
+ import re
710
+
711
711
  info: Dict[str, Any] = {}
712
712
 
713
713
  if not progress_obj:
714
714
  return info
715
715
 
716
- if "status" in progress_obj:
717
- info["status"] = progress_obj["status"]
716
+ # Normalise keys to lowercase so we handle both 'Status'/'status', 'Progress'/'progress', etc.
717
+ norm = {k.lower(): v for k, v in progress_obj.items()}
718
718
 
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
719
+ if "status" in norm:
720
+ info["status"] = norm["status"]
728
721
 
722
+ progress_str = norm.get("progress")
723
+ if progress_str and isinstance(progress_str, str):
724
+ # Try JSON first
725
+ try:
726
+ progress_data = json.loads(progress_str)
729
727
  if "iteration" in progress_data:
730
728
  info["iteration"] = progress_data["iteration"]
731
729
  if "percentage" in progress_data:
@@ -733,18 +731,23 @@ def parse_progress_data(progress_obj: Optional[Dict[str, Any]]) -> Dict[str, Any
733
731
  if "max_iterations" in progress_data:
734
732
  info["max_iterations"] = progress_data["max_iterations"]
735
733
  except (json.JSONDecodeError, TypeError, ValueError):
736
- pass
737
-
738
- # Parse Data JSON string
739
- data_str = progress_obj.get("data")
740
- if data_str:
734
+ # Parse human-readable summary text
735
+ m = re.search(r'Last Iteration\s*:\s*(\d+)\s*/\s*(\d+)', progress_str)
736
+ if m:
737
+ info["iteration"] = int(m.group(1))
738
+ info["max_iterations"] = int(m.group(2))
739
+ m = re.search(r'Final Residual\s*:\s*([\d.E+\-]+)', progress_str, re.IGNORECASE)
740
+ if m:
741
+ info["residual_norm"] = float(m.group(1))
742
+ m = re.search(r'Time/iter\s*:\s*([\d.]+)\s*ms', progress_str)
743
+ if m:
744
+ info["time_per_iter_ms"] = float(m.group(1))
745
+
746
+ # Parse data field if present (JSON dict or string)
747
+ data_val = norm.get("data")
748
+ if data_val:
741
749
  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
-
750
+ data_obj = json.loads(data_val) if isinstance(data_val, str) else data_val
748
751
  if "residual_norm" in data_obj:
749
752
  info["residual_norm"] = data_obj["residual_norm"]
750
753
  if "residual_norm_threshold" in data_obj:
@@ -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}")
@@ -103,10 +103,10 @@ def simulate(
103
103
  api_key = _defaults["api_key"] or _api_key
104
104
  api_url = _defaults["api_url"] or _api_url
105
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."
106
+ raise SystemExit(
107
+ "\nwavesimpro: no API key configured.\n"
108
+ "Run the setup wizard once to get started:\n\n"
109
+ " python -m wavesimpro setup\n"
110
110
  )
111
111
  description = _defaults["description"]
112
112
  poll_interval = _defaults["poll_interval"]
@@ -133,10 +133,8 @@ def simulate(
133
133
  def _progress_cb(info: dict):
134
134
  _last["iteration"] = info.get("iteration", _last["iteration"])
135
135
  _last["residual_norm"] = info.get("residual_norm", _last["residual_norm"])
136
- if callback is not None:
136
+ if callback is not None and _last["iteration"] is not None:
137
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
138
  callback(None, _last["iteration"], None, _last["residual_norm"], **extra)
141
139
 
142
140
  E, _ = execute_via_api(
@@ -159,4 +157,7 @@ def simulate(
159
157
  progress_callback=_progress_cb,
160
158
  )
161
159
 
162
- return E, _last["iteration"], _last["residual_norm"]
160
+ iterations = _last["iteration"] or 1
161
+ 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
163
+ 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.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