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.
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/PKG-INFO +1 -1
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/pyproject.toml +1 -1
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/execute.py +90 -22
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/json_helper.py +32 -29
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/setup.py +12 -11
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/simulate.py +9 -8
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/validate.py +6 -4
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro.egg-info/PKG-INFO +1 -1
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/setup.cfg +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/__init__.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/__main__.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/binary_utils.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/client.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/config.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/exceptions.py +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro/py.typed +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro.egg-info/SOURCES.txt +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro.egg-info/dependency_links.txt +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/src/wavesimpro.egg-info/requires.txt +0 -0
- {wavesimpro-0.9.0 → wavesimpro-0.9.1}/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.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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
351
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
439
|
+
if status_str != "Completed":
|
|
378
440
|
# Build detailed error message
|
|
379
|
-
error_parts = [f"Simulation ended with 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 =
|
|
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
|
|
702
|
+
Parse the progress dict from get_command_status().
|
|
703
703
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
717
|
-
|
|
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
|
-
|
|
720
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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(
|
|
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
|
|
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}")
|
|
@@ -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
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|