wavesimpro 0.9.0__py3-none-any.whl
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/__init__.py +98 -0
- wavesimpro/__main__.py +29 -0
- wavesimpro/binary_utils.py +936 -0
- wavesimpro/client.py +1475 -0
- wavesimpro/config.py +123 -0
- wavesimpro/exceptions.py +40 -0
- wavesimpro/execute.py +461 -0
- wavesimpro/json_helper.py +755 -0
- wavesimpro/py.typed +4 -0
- wavesimpro/setup.py +162 -0
- wavesimpro/simulate.py +162 -0
- wavesimpro/validate.py +251 -0
- wavesimpro-0.9.0.dist-info/METADATA +21 -0
- wavesimpro-0.9.0.dist-info/RECORD +16 -0
- wavesimpro-0.9.0.dist-info/WHEEL +5 -0
- wavesimpro-0.9.0.dist-info/top_level.txt +1 -0
wavesimpro/py.typed
ADDED
wavesimpro/setup.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Setup wizard and credential management for wavesimPro_api.
|
|
3
|
+
|
|
4
|
+
Global config lives at ~/.wavesimPro_api/.env and is written by run_setup().
|
|
5
|
+
Credential resolution order:
|
|
6
|
+
1. configure() call
|
|
7
|
+
2. Local .env in the current working directory
|
|
8
|
+
3. ~/.wavesimPro_api/.env (global, written by this wizard)
|
|
9
|
+
4. RAYFOS_API_KEY / RAYFOS_API_URL environment variables
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import getpass
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
DEFAULT_API_URL = "https://server.rayfos.com"
|
|
17
|
+
GLOBAL_CONFIG_DIR = Path.home() / ".wavesimPro_api"
|
|
18
|
+
GLOBAL_CONFIG_FILE = GLOBAL_CONFIG_DIR / ".env"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_global_config() -> dict:
|
|
22
|
+
"""Load key/url from the global config file if it exists."""
|
|
23
|
+
result = {}
|
|
24
|
+
if GLOBAL_CONFIG_FILE.exists():
|
|
25
|
+
for line in GLOBAL_CONFIG_FILE.read_text(encoding="utf-8").splitlines():
|
|
26
|
+
line = line.strip()
|
|
27
|
+
if line and not line.startswith("#") and "=" in line:
|
|
28
|
+
k, _, v = line.partition("=")
|
|
29
|
+
result[k.strip()] = v.strip()
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def save_global_config(api_key: str, api_url: str):
|
|
34
|
+
GLOBAL_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
GLOBAL_CONFIG_FILE.write_text(
|
|
36
|
+
f"RAYFOS_API_KEY={api_key}\n"
|
|
37
|
+
f"RAYFOS_API_URL={api_url}\n",
|
|
38
|
+
encoding="utf-8",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_credentials() -> tuple[str | None, str]:
|
|
43
|
+
"""Return (api_key, api_url) from the first source that has them."""
|
|
44
|
+
from . import _defaults # module-level configure() values
|
|
45
|
+
|
|
46
|
+
api_key = _defaults.get("api_key")
|
|
47
|
+
api_url = _defaults.get("api_url")
|
|
48
|
+
if api_key:
|
|
49
|
+
return api_key, api_url or DEFAULT_API_URL
|
|
50
|
+
|
|
51
|
+
# Local .env
|
|
52
|
+
local_env = Path(".env")
|
|
53
|
+
if local_env.exists():
|
|
54
|
+
from dotenv import dotenv_values
|
|
55
|
+
vals = dotenv_values(local_env)
|
|
56
|
+
api_key = api_key or vals.get("RAYFOS_API_KEY")
|
|
57
|
+
api_url = api_url or vals.get("RAYFOS_API_URL")
|
|
58
|
+
if api_key:
|
|
59
|
+
return api_key, api_url or DEFAULT_API_URL
|
|
60
|
+
|
|
61
|
+
# Global config
|
|
62
|
+
cfg = load_global_config()
|
|
63
|
+
api_key = api_key or cfg.get("RAYFOS_API_KEY")
|
|
64
|
+
api_url = api_url or cfg.get("RAYFOS_API_URL")
|
|
65
|
+
if api_key:
|
|
66
|
+
return api_key, api_url or DEFAULT_API_URL
|
|
67
|
+
|
|
68
|
+
# Environment variables
|
|
69
|
+
api_key = os.environ.get("RAYFOS_API_KEY")
|
|
70
|
+
api_url = api_url or os.environ.get("RAYFOS_API_URL")
|
|
71
|
+
return api_key, api_url or DEFAULT_API_URL
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _test_connection(api_key: str, api_url: str) -> bool:
|
|
75
|
+
try:
|
|
76
|
+
import requests
|
|
77
|
+
resp = requests.get(
|
|
78
|
+
f"{api_url.rstrip('/')}/api/health",
|
|
79
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
80
|
+
timeout=10,
|
|
81
|
+
verify=not ("localhost" in api_url or "127.0.0.1" in api_url),
|
|
82
|
+
)
|
|
83
|
+
return resp.status_code < 500
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f" Connection error: {e}")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_setup():
|
|
90
|
+
print()
|
|
91
|
+
print("wavesimPro_api — setup wizard")
|
|
92
|
+
print("=" * 40)
|
|
93
|
+
|
|
94
|
+
existing = load_global_config()
|
|
95
|
+
existing_key = existing.get("RAYFOS_API_KEY", "")
|
|
96
|
+
existing_url = existing.get("RAYFOS_API_URL", DEFAULT_API_URL)
|
|
97
|
+
|
|
98
|
+
if existing_key:
|
|
99
|
+
masked = existing_key[:6] + "..." + existing_key[-4:]
|
|
100
|
+
print(f"Existing config found at: {GLOBAL_CONFIG_FILE}")
|
|
101
|
+
print(f" API key : {masked}")
|
|
102
|
+
print(f" Server : {existing_url}")
|
|
103
|
+
print()
|
|
104
|
+
overwrite = input("Overwrite? [y/N]: ").strip().lower()
|
|
105
|
+
if overwrite != "y":
|
|
106
|
+
print("Keeping existing config.")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
print()
|
|
110
|
+
api_key = getpass.getpass("API key (input hidden): ").strip()
|
|
111
|
+
if not api_key:
|
|
112
|
+
print("No API key entered. Aborting.")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
url_prompt = f"Server URL [{DEFAULT_API_URL}]: "
|
|
116
|
+
api_url = input(url_prompt).strip() or DEFAULT_API_URL
|
|
117
|
+
|
|
118
|
+
print()
|
|
119
|
+
print("Testing connection...")
|
|
120
|
+
ok = _test_connection(api_key, api_url)
|
|
121
|
+
if ok:
|
|
122
|
+
print(" Connection successful.")
|
|
123
|
+
else:
|
|
124
|
+
print(" Could not reach server — saving anyway (check URL/key later).")
|
|
125
|
+
|
|
126
|
+
save_global_config(api_key, api_url)
|
|
127
|
+
print()
|
|
128
|
+
print(f"Config saved to: {GLOBAL_CONFIG_FILE}")
|
|
129
|
+
print("You're all set. Import wavesimPro_api in any project without extra setup.")
|
|
130
|
+
print()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def run_status():
|
|
134
|
+
print()
|
|
135
|
+
print("wavesimPro_api — current configuration")
|
|
136
|
+
print("=" * 40)
|
|
137
|
+
|
|
138
|
+
api_key, api_url = resolve_credentials()
|
|
139
|
+
|
|
140
|
+
if not api_key:
|
|
141
|
+
print("No credentials found.")
|
|
142
|
+
print(f"Run: python -m wavesimPro_api setup")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
masked = api_key[:6] + "..." + api_key[-4:] if len(api_key) > 10 else "***"
|
|
146
|
+
print(f"API key : {masked}")
|
|
147
|
+
print(f"Server : {api_url}")
|
|
148
|
+
|
|
149
|
+
# Show source
|
|
150
|
+
cfg = load_global_config()
|
|
151
|
+
if cfg.get("RAYFOS_API_KEY") == api_key:
|
|
152
|
+
print(f"Source : {GLOBAL_CONFIG_FILE}")
|
|
153
|
+
elif Path(".env").exists():
|
|
154
|
+
print("Source : local .env")
|
|
155
|
+
else:
|
|
156
|
+
print("Source : environment variable")
|
|
157
|
+
|
|
158
|
+
print()
|
|
159
|
+
print("Testing connection...")
|
|
160
|
+
ok = _test_connection(api_key, api_url)
|
|
161
|
+
print(" OK" if ok else " Failed — check your API key and server URL")
|
|
162
|
+
print()
|
wavesimpro/simulate.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
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"]
|
wavesimpro/validate.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parameter validation for Wavesim simulations
|
|
3
|
+
|
|
4
|
+
Ported from MATLAB validateParameters.m. Validates all simulation parameters
|
|
5
|
+
before uploading files to the server, providing detailed error/warning messages.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
import logging
|
|
10
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ParameterValidationError(Exception):
|
|
18
|
+
"""Raised when parameter validation fails with one or more errors."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, errors: List[str], warnings: List[str] = None):
|
|
21
|
+
self.errors = errors
|
|
22
|
+
self.warnings = warnings or []
|
|
23
|
+
msg = f"Parameter validation failed with {len(errors)} error(s):\n - " + "\n - ".join(errors)
|
|
24
|
+
super().__init__(msg)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_parameters(
|
|
28
|
+
refractive_index: np.ndarray,
|
|
29
|
+
sources: List[Dict[str, Any]],
|
|
30
|
+
wavelength: float,
|
|
31
|
+
pixel_size: float,
|
|
32
|
+
threshold: float = 1e-2,
|
|
33
|
+
max_iterations: int = 1000,
|
|
34
|
+
boundary_width: int = 0,
|
|
35
|
+
is_vectorial: bool = True,
|
|
36
|
+
) -> List[str]:
|
|
37
|
+
"""
|
|
38
|
+
Validate simulation parameters before upload.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
refractive_index: [nx, ny, nz] array (refractive index or permittivity)
|
|
42
|
+
sources: List of source dicts with 'field', 'position', 'polarization' keys
|
|
43
|
+
wavelength: Wavelength in micrometers
|
|
44
|
+
pixel_size: Pixel size in micrometers
|
|
45
|
+
threshold: Convergence threshold
|
|
46
|
+
max_iterations: Maximum iterations
|
|
47
|
+
boundary_width: Boundary width in pixels
|
|
48
|
+
is_vectorial: True for Maxwell, False for Helmholtz
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of warning strings (empty if no warnings)
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ParameterValidationError: If any parameter is invalid
|
|
55
|
+
"""
|
|
56
|
+
errors: List[str] = []
|
|
57
|
+
warnings: List[str] = []
|
|
58
|
+
|
|
59
|
+
logger.info("Validating simulation parameters...")
|
|
60
|
+
|
|
61
|
+
# 1. Validate refractive index / permittivity array
|
|
62
|
+
if refractive_index is None or refractive_index.size == 0:
|
|
63
|
+
errors.append("Refractive index array is empty")
|
|
64
|
+
else:
|
|
65
|
+
shape = refractive_index.shape
|
|
66
|
+
# Pad to 3D
|
|
67
|
+
while len(shape) < 3:
|
|
68
|
+
shape = shape + (1,)
|
|
69
|
+
nx, ny, nz = shape[0], shape[1], shape[2]
|
|
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]")
|
|
73
|
+
|
|
74
|
+
if np.any(np.isnan(refractive_index)):
|
|
75
|
+
errors.append("Refractive index contains NaN values")
|
|
76
|
+
if np.any(np.isinf(refractive_index)):
|
|
77
|
+
errors.append("Refractive index contains Inf values")
|
|
78
|
+
|
|
79
|
+
min_real = float(np.min(np.real(refractive_index)))
|
|
80
|
+
max_real = float(np.max(np.real(refractive_index)))
|
|
81
|
+
if min_real <= 0:
|
|
82
|
+
warnings.append(f"Refractive index has non-positive real values (min={min_real:.4f}). This may cause issues.")
|
|
83
|
+
if max_real > 10:
|
|
84
|
+
warnings.append(f"Refractive index has very high values (max={max_real:.4f}). Verify this is correct.")
|
|
85
|
+
|
|
86
|
+
logger.info(f" Domain: [{nx} x {ny} x {nz}]")
|
|
87
|
+
|
|
88
|
+
# 2. Validate wavelength
|
|
89
|
+
if wavelength is None:
|
|
90
|
+
errors.append("Wavelength is required")
|
|
91
|
+
elif wavelength <= 0:
|
|
92
|
+
errors.append(f"Wavelength must be positive (got {wavelength:.4f})")
|
|
93
|
+
elif wavelength < 0.1 or wavelength > 100:
|
|
94
|
+
warnings.append(f"Unusual wavelength: {wavelength:.4f} um. Typical range is 0.1-100 um.")
|
|
95
|
+
|
|
96
|
+
# 3. Validate pixel size
|
|
97
|
+
if pixel_size is None:
|
|
98
|
+
errors.append("Pixel size is required")
|
|
99
|
+
elif pixel_size <= 0:
|
|
100
|
+
errors.append(f"Pixel size must be positive (got {pixel_size:.4f})")
|
|
101
|
+
elif refractive_index is not None and refractive_index.size > 0 and wavelength and wavelength > 0:
|
|
102
|
+
n_max = float(np.max(np.real(refractive_index)))
|
|
103
|
+
if n_max > 0:
|
|
104
|
+
nyquist_limit = wavelength / (2 * n_max)
|
|
105
|
+
if pixel_size > nyquist_limit:
|
|
106
|
+
warnings.append(
|
|
107
|
+
f"Pixel size ({pixel_size:.4f} um) exceeds Nyquist limit "
|
|
108
|
+
f"({nyquist_limit:.4f} um for n_max={n_max:.2f}). Results may be inaccurate."
|
|
109
|
+
)
|
|
110
|
+
pixels_per_wavelength = wavelength / (n_max * pixel_size)
|
|
111
|
+
if pixels_per_wavelength < 10:
|
|
112
|
+
warnings.append(
|
|
113
|
+
f"Only {pixels_per_wavelength:.1f} pixels per wavelength in "
|
|
114
|
+
f"highest-index medium. Recommend >= 10."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# 4. Validate threshold
|
|
118
|
+
if threshold is None:
|
|
119
|
+
errors.append("Threshold must be numeric")
|
|
120
|
+
elif threshold <= 0:
|
|
121
|
+
errors.append(f"Threshold must be positive (got {threshold:.2e})")
|
|
122
|
+
elif threshold > 1:
|
|
123
|
+
warnings.append(f"Threshold ({threshold:.2e}) is very high. Typical range is 1e-9 to 1e-2.")
|
|
124
|
+
elif threshold < 1e-12:
|
|
125
|
+
warnings.append(f"Threshold ({threshold:.2e}) is very low. May not converge within max iterations.")
|
|
126
|
+
|
|
127
|
+
# 5. Validate max iterations
|
|
128
|
+
if max_iterations is None:
|
|
129
|
+
errors.append("Max iterations must be numeric")
|
|
130
|
+
elif max_iterations < 1:
|
|
131
|
+
errors.append(f"Max iterations must be >= 1 (got {max_iterations})")
|
|
132
|
+
elif max_iterations < 100:
|
|
133
|
+
warnings.append(f"Max iterations ({max_iterations}) is low. May not converge.")
|
|
134
|
+
elif max_iterations > 100000:
|
|
135
|
+
warnings.append(f"Max iterations ({max_iterations}) is very high. Simulation may take a long time.")
|
|
136
|
+
|
|
137
|
+
# 6. Validate boundary width
|
|
138
|
+
if boundary_width is not None:
|
|
139
|
+
if boundary_width < 0:
|
|
140
|
+
errors.append(f"Boundary width must be >= 0 (got {boundary_width})")
|
|
141
|
+
elif boundary_width > 0 and refractive_index is not None and refractive_index.size > 0:
|
|
142
|
+
shape = refractive_index.shape
|
|
143
|
+
while len(shape) < 3:
|
|
144
|
+
shape = shape + (1,)
|
|
145
|
+
dims = shape[:3]
|
|
146
|
+
dim_names = ("x", "y", "z")
|
|
147
|
+
exceeded = []
|
|
148
|
+
for i in range(3):
|
|
149
|
+
max_allowed = dims[i] // 2
|
|
150
|
+
if boundary_width > max_allowed:
|
|
151
|
+
exceeded.append(f"{dim_names[i]} ({dims[i]}->{max_allowed})")
|
|
152
|
+
if exceeded:
|
|
153
|
+
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
|
+
)
|
|
157
|
+
|
|
158
|
+
# 7. Validate GPU memory requirements
|
|
159
|
+
if refractive_index is not None and refractive_index.size > 0:
|
|
160
|
+
shape = refractive_index.shape
|
|
161
|
+
while len(shape) < 3:
|
|
162
|
+
shape = shape + (1,)
|
|
163
|
+
nx, ny, nz = shape[0], shape[1], shape[2]
|
|
164
|
+
|
|
165
|
+
total_nx = nx + 2 * boundary_width
|
|
166
|
+
total_ny = ny + 2 * boundary_width
|
|
167
|
+
total_nz = nz + 2 * boundary_width
|
|
168
|
+
total_voxels = total_nx * total_ny * total_nz
|
|
169
|
+
|
|
170
|
+
bytes_per_voxel = 120 if is_vectorial else 40
|
|
171
|
+
gpu_memory_gb = (total_voxels * bytes_per_voxel) / (1024 ** 3)
|
|
172
|
+
|
|
173
|
+
GPU_MEMORY_OK_GB = 16
|
|
174
|
+
GPU_MEMORY_MAX_GB = 65
|
|
175
|
+
|
|
176
|
+
logger.info(f" GPU memory estimate: {gpu_memory_gb:.2f} GB ({'Vectorial' if is_vectorial else 'Scalar'})")
|
|
177
|
+
|
|
178
|
+
if gpu_memory_gb > GPU_MEMORY_MAX_GB:
|
|
179
|
+
errors.append(
|
|
180
|
+
f"Domain size too large for GPU. Estimated memory: {gpu_memory_gb:.2f} GB "
|
|
181
|
+
f"exceeds maximum {GPU_MEMORY_MAX_GB:.0f} GB. Reduce domain size or pixel count."
|
|
182
|
+
)
|
|
183
|
+
elif gpu_memory_gb > GPU_MEMORY_OK_GB:
|
|
184
|
+
warnings.append(
|
|
185
|
+
f"GPU memory {gpu_memory_gb:.2f} GB exceeds standard {GPU_MEMORY_OK_GB:.0f} GB. "
|
|
186
|
+
f"Will use advanced GPU instance."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# 8. Validate sources
|
|
190
|
+
if not sources:
|
|
191
|
+
errors.append("At least one source is required")
|
|
192
|
+
else:
|
|
193
|
+
ri_shape = refractive_index.shape if refractive_index is not None else (0, 0, 0)
|
|
194
|
+
while len(ri_shape) < 3:
|
|
195
|
+
ri_shape = ri_shape + (1,)
|
|
196
|
+
nx, ny, nz = ri_shape[0], ri_shape[1], ri_shape[2]
|
|
197
|
+
|
|
198
|
+
for i, src in enumerate(sources):
|
|
199
|
+
src_field = src.get("field")
|
|
200
|
+
if src_field is None or (isinstance(src_field, np.ndarray) and src_field.size == 0):
|
|
201
|
+
errors.append(f"Source {i + 1}: field is empty")
|
|
202
|
+
elif isinstance(src_field, np.ndarray):
|
|
203
|
+
if np.any(np.isnan(src_field)):
|
|
204
|
+
errors.append(f"Source {i + 1}: field contains NaN values")
|
|
205
|
+
if np.any(np.isinf(src_field)):
|
|
206
|
+
errors.append(f"Source {i + 1}: field contains Inf values")
|
|
207
|
+
|
|
208
|
+
src_shape = src_field.shape
|
|
209
|
+
while len(src_shape) < 3:
|
|
210
|
+
src_shape = src_shape + (1,)
|
|
211
|
+
if src_shape[0] > nx or src_shape[1] > ny or src_shape[2] > nz:
|
|
212
|
+
errors.append(
|
|
213
|
+
f"Source {i + 1}: field size [{src_shape[0]},{src_shape[1]},{src_shape[2]}] "
|
|
214
|
+
f"exceeds domain [{nx},{ny},{nz}]"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if np.all(src_field == 0):
|
|
218
|
+
warnings.append(f"Source {i + 1}: field is all zeros")
|
|
219
|
+
|
|
220
|
+
# Check position
|
|
221
|
+
pos = src.get("position")
|
|
222
|
+
if pos is None or len(pos) < 3:
|
|
223
|
+
errors.append(f"Source {i + 1}: position must have at least 3 elements [x, y, z]")
|
|
224
|
+
elif pixel_size and pixel_size > 0:
|
|
225
|
+
domain_x = nx * pixel_size
|
|
226
|
+
domain_y = ny * pixel_size
|
|
227
|
+
domain_z = nz * pixel_size
|
|
228
|
+
if pos[0] < 0 or pos[0] > domain_x:
|
|
229
|
+
warnings.append(f"Source {i + 1}: x position ({pos[0]:.2f} um) outside domain [0, {domain_x:.2f}]")
|
|
230
|
+
if pos[1] < 0 or pos[1] > domain_y:
|
|
231
|
+
warnings.append(f"Source {i + 1}: y position ({pos[1]:.2f} um) outside domain [0, {domain_y:.2f}]")
|
|
232
|
+
if pos[2] < 0 or pos[2] > domain_z:
|
|
233
|
+
warnings.append(f"Source {i + 1}: z position ({pos[2]:.2f} um) outside domain [0, {domain_z:.2f}]")
|
|
234
|
+
|
|
235
|
+
# Check polarization for vectorial
|
|
236
|
+
pol = src.get("polarization")
|
|
237
|
+
if pol is not None and is_vectorial and pol not in (0, 1, 2):
|
|
238
|
+
warnings.append(f"Source {i + 1}: invalid polarization {pol} for vectorial simulation (expected 0=x, 1=y, 2=z)")
|
|
239
|
+
|
|
240
|
+
# Report warnings
|
|
241
|
+
for w in warnings:
|
|
242
|
+
logger.warning(f" [!] {w}")
|
|
243
|
+
|
|
244
|
+
# Raise on errors
|
|
245
|
+
if errors:
|
|
246
|
+
for e in errors:
|
|
247
|
+
logger.error(f" [X] {e}")
|
|
248
|
+
raise ParameterValidationError(errors, warnings)
|
|
249
|
+
|
|
250
|
+
logger.info(" All parameters validated successfully")
|
|
251
|
+
return warnings
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wavesimpro
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: WavesimPro Python API — cloud execution client for the Rayfos simulation platform
|
|
5
|
+
Author: Rayfos
|
|
6
|
+
Project-URL: Repository, https://github.com/rayfos/wavesimpro
|
|
7
|
+
Keywords: wavesim,simulation,electromagnetic,rayfos,api
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: numpy>=1.20.0
|
|
16
|
+
Requires-Dist: requests>=2.28.0
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
20
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
21
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
wavesimpro/__init__.py,sha256=Xs16Y1JTqnR7U1v2iIL2JdDyrFx2np8enuAA9BmW2Ms,2637
|
|
2
|
+
wavesimpro/__main__.py,sha256=bFC1Az6fsX2Rov-z90K-Dqcl8X74RXgIG_RudRBfO1k,600
|
|
3
|
+
wavesimpro/binary_utils.py,sha256=CF8ucmPdcK2tlvogDRO-PJKJo7PCoq5OPcSKIvGzgic,35106
|
|
4
|
+
wavesimpro/client.py,sha256=qm60X3PWOYQk-8GRs49KcHWwnEZarVIOa2Gh-_Al7iI,55654
|
|
5
|
+
wavesimpro/config.py,sha256=MVghdqd5rxsvAhiR3qipzZnPbSDfDjeD27ypyV1Ia6s,4442
|
|
6
|
+
wavesimpro/exceptions.py,sha256=yb4XN52Z4rf5MMANV01nexfH4udC9X_aorC9j4H5pG4,903
|
|
7
|
+
wavesimpro/execute.py,sha256=Uu4qkHylfWyXSyW_6adiMlgcWWHP0r34dSb424FtHrM,18451
|
|
8
|
+
wavesimpro/json_helper.py,sha256=OOdnVyMU4WwLAdUtiOPtHjrS-4hZ0GCfwQzM8gAtC70,23416
|
|
9
|
+
wavesimpro/py.typed,sha256=nJJj1nmMhDHzYPOcAJC53joZRqpJr7vghugcsQrE09s,87
|
|
10
|
+
wavesimpro/setup.py,sha256=XIUAbfxaamXo5mSZdb9rXBxaPMe3dAzZCZkd9GUP_M4,5071
|
|
11
|
+
wavesimpro/simulate.py,sha256=4ZReFDnKndyacpg_rkJDq__8tqSa6ho6BV0QvKSwzJE,6290
|
|
12
|
+
wavesimpro/validate.py,sha256=d4WdTwCBqZsU0xbYxRd02OF4GBvSdPazqMS0bgCm2EY,10759
|
|
13
|
+
wavesimpro-0.9.0.dist-info/METADATA,sha256=kWukzFa_Y0gXVvv3oXjzASXM4Xu1uOQRnTz43IaNJss,843
|
|
14
|
+
wavesimpro-0.9.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
15
|
+
wavesimpro-0.9.0.dist-info/top_level.txt,sha256=n0QlP2w8juE34aE4Pl_np97zcVWNCpa-LxmypLNvZLQ,11
|
|
16
|
+
wavesimpro-0.9.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wavesimpro
|