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/py.typed ADDED
@@ -0,0 +1,4 @@
1
+ # Marker file for PEP 561
2
+ # This indicates that this package supports type hints
3
+
4
+
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ wavesimpro