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.
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/PKG-INFO +1 -1
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/pyproject.toml +1 -1
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/__init__.py +2 -2
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/client.py +5 -1
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/execute.py +98 -23
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/json_helper.py +57 -30
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/setup.py +12 -11
- wavesimpro-0.9.2/src/wavesimpro/simulate.py +286 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/validate.py +6 -4
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro.egg-info/PKG-INFO +1 -1
- wavesimpro-0.9.0/src/wavesimpro/simulate.py +0 -162
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/setup.cfg +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/__main__.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/binary_utils.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/config.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/exceptions.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro/py.typed +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro.egg-info/SOURCES.txt +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro.egg-info/requires.txt +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.2}/src/wavesimpro.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wavesimpro"
|
|
7
|
-
version = "0.9.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
351
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
446
|
+
if status_str != "Completed":
|
|
378
447
|
# Build detailed error message
|
|
379
|
-
error_parts = [f"Simulation ended with 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 =
|
|
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
|
|
703
|
-
|
|
704
|
-
Args:
|
|
705
|
-
progress_obj: CommandProgressDto dict from get_command_status()
|
|
706
|
+
Parse the progress dict from get_command_status().
|
|
706
707
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
717
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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(
|
|
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
|
|
2
|
+
Setup wizard and credential management for wavesimpro.
|
|
3
3
|
|
|
4
|
-
Global config lives at ~/.
|
|
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. ~/.
|
|
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() / ".
|
|
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
|
|
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("
|
|
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 =
|
|
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
|
|
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("
|
|
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(
|
|
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
|
-
|
|
72
|
-
|
|
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)
|
|
155
|
-
f"dimension(s): {', '.join(exceeded)}.
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|