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/config.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration module for WavesimPythonAPI
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ClientConfig:
|
|
12
|
+
"""
|
|
13
|
+
Configuration for the Rayfos API client
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
api_token: User API token for authentication
|
|
17
|
+
server_url: Base URL of RayfosJobServer for project management (default: server.rayfos.com)
|
|
18
|
+
machine_service_url: Optional URL of RayfosMachineService for file uploads
|
|
19
|
+
timeout: Request timeout in seconds
|
|
20
|
+
verify_ssl: Whether to verify SSL certificates for server_url
|
|
21
|
+
verify_ssl_machine_service: Whether to verify SSL certificates for machine_service_url
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
api_token: str
|
|
25
|
+
server_url: str = "https://server.rayfos.com"
|
|
26
|
+
machine_service_url: Optional[str] = None
|
|
27
|
+
timeout: int = 30
|
|
28
|
+
verify_ssl: bool = True
|
|
29
|
+
verify_ssl_machine_service: bool = True
|
|
30
|
+
|
|
31
|
+
def __post_init__(self):
|
|
32
|
+
"""Validate configuration after initialization"""
|
|
33
|
+
# Remove trailing slashes from URLs
|
|
34
|
+
if self.server_url:
|
|
35
|
+
self.server_url = self.server_url.rstrip('/')
|
|
36
|
+
if self.machine_service_url:
|
|
37
|
+
self.machine_service_url = self.machine_service_url.rstrip('/')
|
|
38
|
+
|
|
39
|
+
# Auto-detect SSL verification for localhost machine service
|
|
40
|
+
if not hasattr(self, '_verify_ssl_ms_set'):
|
|
41
|
+
if "localhost" in self.machine_service_url or "127.0.0.1" in self.machine_service_url:
|
|
42
|
+
self.verify_ssl_machine_service = False
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_env(cls) -> "ClientConfig":
|
|
46
|
+
"""
|
|
47
|
+
Create configuration from environment variables
|
|
48
|
+
|
|
49
|
+
Environment variables:
|
|
50
|
+
RAYFOS_API_TOKEN: API token (required)
|
|
51
|
+
RAYFOS_SERVER_URL: Project server URL (optional, defaults to server.rayfos.com)
|
|
52
|
+
RAYFOS_MACHINE_SERVICE_URL: Machine service URL for file uploads (optional)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
ClientConfig instance
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If required environment variables are missing
|
|
59
|
+
"""
|
|
60
|
+
api_token = os.getenv("RAYFOS_API_TOKEN")
|
|
61
|
+
if not api_token:
|
|
62
|
+
raise ValueError("RAYFOS_API_TOKEN environment variable is required")
|
|
63
|
+
|
|
64
|
+
# Get server URL (defaults to cloud server)
|
|
65
|
+
server_url = os.getenv("RAYFOS_SERVER_URL", "https://server.rayfos.com")
|
|
66
|
+
|
|
67
|
+
# Get optional machine service URL for file uploads
|
|
68
|
+
machine_service_url = os.getenv("RAYFOS_MACHINE_SERVICE_URL")
|
|
69
|
+
|
|
70
|
+
# SSL verification for main server (always True for production servers)
|
|
71
|
+
verify_ssl = True
|
|
72
|
+
if "localhost" in server_url or "127.0.0.1" in server_url:
|
|
73
|
+
verify_ssl = False
|
|
74
|
+
|
|
75
|
+
# SSL verification for machine service (auto-detect localhost)
|
|
76
|
+
verify_ssl_machine_service = True
|
|
77
|
+
if machine_service_url and ("localhost" in machine_service_url or "127.0.0.1" in machine_service_url):
|
|
78
|
+
verify_ssl_machine_service = False
|
|
79
|
+
|
|
80
|
+
return cls(
|
|
81
|
+
api_token=api_token,
|
|
82
|
+
server_url=server_url,
|
|
83
|
+
machine_service_url=machine_service_url,
|
|
84
|
+
timeout=30,
|
|
85
|
+
verify_ssl=verify_ssl,
|
|
86
|
+
verify_ssl_machine_service=verify_ssl_machine_service,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def get_api_url(self, endpoint: str) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Get the full URL for an API endpoint
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
endpoint: API endpoint path (e.g., '/api/projects')
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Full URL
|
|
98
|
+
"""
|
|
99
|
+
if not endpoint.startswith('/'):
|
|
100
|
+
endpoint = f'/{endpoint}'
|
|
101
|
+
return f"{self.server_url}{endpoint}"
|
|
102
|
+
|
|
103
|
+
def get_machine_service_url(self, endpoint: str) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Get the full URL for a machine service endpoint
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
endpoint: Machine service endpoint path
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Full URL
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If machine_service_url is not configured
|
|
115
|
+
"""
|
|
116
|
+
if not self.machine_service_url:
|
|
117
|
+
raise ValueError("Machine service URL is not configured")
|
|
118
|
+
|
|
119
|
+
if not endpoint.startswith('/'):
|
|
120
|
+
endpoint = f'/{endpoint}'
|
|
121
|
+
return f"{self.machine_service_url}{endpoint}"
|
|
122
|
+
|
|
123
|
+
|
wavesimpro/exceptions.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception classes for WavesimPythonAPI
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RayfosAPIError(Exception):
|
|
7
|
+
"""Base exception for all Rayfos API errors"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, status_code: int = None, response=None):
|
|
10
|
+
self.message = message
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
self.response = response
|
|
13
|
+
super().__init__(self.message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthenticationError(RayfosAPIError):
|
|
17
|
+
"""Raised when authentication fails"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NotFoundError(RayfosAPIError):
|
|
22
|
+
"""Raised when a resource is not found (404)"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ValidationError(RayfosAPIError):
|
|
27
|
+
"""Raised when input validation fails (400)"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ServerError(RayfosAPIError):
|
|
32
|
+
"""Raised when the server encounters an error (5xx)"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConfigurationError(RayfosAPIError):
|
|
37
|
+
"""Raised when configuration is invalid"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
wavesimpro/execute.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level single-call execution function for Wavesim simulations.
|
|
3
|
+
|
|
4
|
+
Ported from MATLAB executeViaRayfosAPI.m. Provides a single function that
|
|
5
|
+
handles the full workflow: credential loading, project creation, file upload,
|
|
6
|
+
command submission, polling, result download.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import time
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Tuple, Optional, Callable, Dict, Any, List
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from .client import RayfosClient
|
|
19
|
+
from .binary_utils import (
|
|
20
|
+
upload_array_as_binary_zipped,
|
|
21
|
+
upload_source_field_as_binary_zipped,
|
|
22
|
+
read_binary_field,
|
|
23
|
+
)
|
|
24
|
+
from .json_helper import (
|
|
25
|
+
create_simulation_domain,
|
|
26
|
+
create_medium,
|
|
27
|
+
create_voxels_model_primitive,
|
|
28
|
+
create_custom_field_source,
|
|
29
|
+
create_wavesim_properties,
|
|
30
|
+
parse_progress_data,
|
|
31
|
+
)
|
|
32
|
+
from .validate import validate_parameters
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _extract_file_id(file_info: dict) -> str:
|
|
38
|
+
"""Extract file ID from upload response (handles different field names)."""
|
|
39
|
+
for key in ("fileId", "id", "blobName"):
|
|
40
|
+
if key in file_info:
|
|
41
|
+
return file_info[key]
|
|
42
|
+
raise KeyError(f"No file ID found in upload response. Keys: {list(file_info.keys())}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def execute_via_api(
|
|
46
|
+
refractive_index: np.ndarray,
|
|
47
|
+
source,
|
|
48
|
+
wavelength: float,
|
|
49
|
+
pixel_size: float,
|
|
50
|
+
threshold: float = 1e-2,
|
|
51
|
+
max_iterations: int = 1000,
|
|
52
|
+
boundary_width: int = 0,
|
|
53
|
+
is_vectorial: bool = True,
|
|
54
|
+
data_type: str = "RefractiveIndex",
|
|
55
|
+
api_url: Optional[str] = None,
|
|
56
|
+
api_key: Optional[str] = None,
|
|
57
|
+
periodic: tuple = (False, False, False),
|
|
58
|
+
project_id: Optional[str] = None,
|
|
59
|
+
version_id: Optional[str] = None,
|
|
60
|
+
machine_service_url: Optional[str] = None,
|
|
61
|
+
description: str = "Wavesim Python API",
|
|
62
|
+
poll_interval: float = 10.0,
|
|
63
|
+
progress_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
64
|
+
engine_preference: str = "Any",
|
|
65
|
+
use_gpu: Optional[bool] = None,
|
|
66
|
+
) -> Tuple[np.ndarray, bool]:
|
|
67
|
+
"""
|
|
68
|
+
Execute a Wavesim simulation via the Rayfos REST API.
|
|
69
|
+
|
|
70
|
+
Single-call function that handles the full simulation workflow.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
refractive_index: [nx, ny, nz] array (refractive index or permittivity)
|
|
74
|
+
source: Source specification. Can be:
|
|
75
|
+
- dict with 'Es' (complex field array) and 'position' ([x,y,z] or [x,y,z,pol])
|
|
76
|
+
- list of such dicts for multiple sources
|
|
77
|
+
wavelength: Wavelength in micrometers
|
|
78
|
+
pixel_size: Pixel size in micrometers
|
|
79
|
+
threshold: Convergence threshold (default: 1e-2)
|
|
80
|
+
max_iterations: Max iterations (default: 1000)
|
|
81
|
+
boundary_width: Boundary width in pixels (default: 0)
|
|
82
|
+
is_vectorial: True for Maxwell, False for Helmholtz (default: True)
|
|
83
|
+
data_type: 'RefractiveIndex' or 'Permittivity' (default: 'RefractiveIndex')
|
|
84
|
+
api_url: API base URL (default: from RAYFOS_API_URL env var)
|
|
85
|
+
api_key: API key (default: from RAYFOS_API_KEY env var)
|
|
86
|
+
project_id: Existing project ID (default: auto-create)
|
|
87
|
+
version_id: Existing version ID (default: auto-create)
|
|
88
|
+
machine_service_url: Machine service URL (default: from RAYFOS_MACHINE_SERVICE_URL env var)
|
|
89
|
+
description: Command description
|
|
90
|
+
poll_interval: Polling interval in seconds (default: 10.0)
|
|
91
|
+
progress_callback: Optional callback(progress_dict) called during polling
|
|
92
|
+
engine_preference: Engine to use — 'Any' (default), 'Python', or 'Cpp'.
|
|
93
|
+
'Any' lets the job server pick the best available machine.
|
|
94
|
+
Ignored when machine_service_url is set (local mode always uses that machine).
|
|
95
|
+
use_gpu: GPU selection — None (auto, default), True (require GPU), False (force CPU).
|
|
96
|
+
Ignored when machine_service_url is set.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Tuple of (E_field, converged):
|
|
100
|
+
- E_field: Complex output field array. Vectorial: [3, nx, ny, nz], Scalar: [nx, ny, nz]
|
|
101
|
+
- converged: True if simulation completed successfully
|
|
102
|
+
"""
|
|
103
|
+
# 1. Load credentials
|
|
104
|
+
api_url = api_url or os.environ.get("RAYFOS_API_URL", "http://localhost:5159")
|
|
105
|
+
api_key = api_key or os.environ.get("RAYFOS_API_KEY")
|
|
106
|
+
if not api_key:
|
|
107
|
+
raise ValueError("API key must be provided or set in RAYFOS_API_KEY environment variable")
|
|
108
|
+
|
|
109
|
+
project_id = project_id or os.environ.get("RAYFOS_PROJECT_ID")
|
|
110
|
+
version_id = version_id or os.environ.get("RAYFOS_VERSION_ID")
|
|
111
|
+
machine_service_url = machine_service_url or os.environ.get("RAYFOS_MACHINE_SERVICE_URL")
|
|
112
|
+
|
|
113
|
+
# Auto-detect SSL
|
|
114
|
+
verify_ssl = not ("localhost" in api_url.lower() or "127.0.0.1" in api_url)
|
|
115
|
+
verify_ssl_machine = True
|
|
116
|
+
if machine_service_url:
|
|
117
|
+
verify_ssl_machine = not ("localhost" in machine_service_url.lower() or "127.0.0.1" in machine_service_url)
|
|
118
|
+
|
|
119
|
+
logger.info("=" * 60)
|
|
120
|
+
logger.info("Wavesim Python API - Rayfos REST Execution")
|
|
121
|
+
logger.info("=" * 60)
|
|
122
|
+
logger.info(f" Mode: {'Vectorial (Maxwell)' if is_vectorial else 'Scalar (Helmholtz)'}")
|
|
123
|
+
logger.info(f" API URL: {api_url}")
|
|
124
|
+
logger.info(f" Engine: {engine_preference} GPU: {'auto' if use_gpu is None else ('GPU' if use_gpu else 'CPU')}")
|
|
125
|
+
|
|
126
|
+
# 2. Extract domain info
|
|
127
|
+
nx, ny, nz = refractive_index.shape[:3]
|
|
128
|
+
|
|
129
|
+
logger.info(f" Domain: [{nx}, {ny}, {nz}]")
|
|
130
|
+
logger.info(f" Wavelength: {wavelength:.3f} um")
|
|
131
|
+
logger.info(f" Pixel size: {pixel_size:.3f} um")
|
|
132
|
+
logger.info(f" Threshold: {threshold:.2e}")
|
|
133
|
+
logger.info(f" Max iterations: {max_iterations}")
|
|
134
|
+
logger.info(f" Boundary width: {boundary_width} pixels")
|
|
135
|
+
|
|
136
|
+
# 3. Prepare electromagnetic data
|
|
137
|
+
if data_type == "RefractiveIndex":
|
|
138
|
+
background_ri = float(np.min(np.real(refractive_index)))
|
|
139
|
+
else:
|
|
140
|
+
background_ri = float(np.sqrt(np.min(np.real(refractive_index))))
|
|
141
|
+
em_data = refractive_index
|
|
142
|
+
|
|
143
|
+
# 4. Prepare sources
|
|
144
|
+
if isinstance(source, dict):
|
|
145
|
+
source = [source]
|
|
146
|
+
elif not isinstance(source, list):
|
|
147
|
+
source = [source]
|
|
148
|
+
|
|
149
|
+
sources_info = []
|
|
150
|
+
for i, src_item in enumerate(source):
|
|
151
|
+
es = src_item["Es"]
|
|
152
|
+
position = src_item["position"]
|
|
153
|
+
|
|
154
|
+
# Extract polarization
|
|
155
|
+
if len(position) >= 4:
|
|
156
|
+
pol_matlab = position[3]
|
|
157
|
+
if is_vectorial:
|
|
158
|
+
pol_python = int(pol_matlab) - 1 # MATLAB 1-based -> Python 0-based
|
|
159
|
+
else:
|
|
160
|
+
pol_python = -1
|
|
161
|
+
else:
|
|
162
|
+
pol_python = 0 if is_vectorial else -1
|
|
163
|
+
|
|
164
|
+
src_x_um = float(position[0])
|
|
165
|
+
src_y_um = float(position[1])
|
|
166
|
+
src_z_um = float(position[2])
|
|
167
|
+
|
|
168
|
+
sources_info.append({
|
|
169
|
+
"field": es,
|
|
170
|
+
"position": [src_x_um, src_y_um, src_z_um],
|
|
171
|
+
"polarization": pol_python,
|
|
172
|
+
})
|
|
173
|
+
logger.info(
|
|
174
|
+
f" Source {i + 1}: pos=[{src_x_um:.4f}, {src_y_um:.4f}, {src_z_um:.4f}] um, "
|
|
175
|
+
f"shape={es.shape}, pol={pol_python}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# 5. Create client
|
|
179
|
+
logger.info("Connecting to Rayfos API...")
|
|
180
|
+
client = RayfosClient(
|
|
181
|
+
api_token=api_key,
|
|
182
|
+
server_url=api_url,
|
|
183
|
+
machine_service_url=machine_service_url,
|
|
184
|
+
verify_ssl=verify_ssl,
|
|
185
|
+
verify_ssl_machine_service=verify_ssl_machine,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# 6. Create or reuse project/version
|
|
189
|
+
auto_create = not project_id or not version_id
|
|
190
|
+
if auto_create:
|
|
191
|
+
logger.info("Creating project...")
|
|
192
|
+
project_result = client.create_project(
|
|
193
|
+
name=f"{description} - Wavesim Python API",
|
|
194
|
+
app="wavesim",
|
|
195
|
+
description=f"Multi-source simulation ({len(sources_info)} source(s))",
|
|
196
|
+
)
|
|
197
|
+
project_id = project_result["id"]
|
|
198
|
+
versions = project_result.get("versions", [])
|
|
199
|
+
if not versions:
|
|
200
|
+
raise RuntimeError("No version found in created project")
|
|
201
|
+
version_result = versions[0]
|
|
202
|
+
version_id = version_result["id"]
|
|
203
|
+
properties_id = version_result["propertiesId"]
|
|
204
|
+
logger.info(f" Project ID: {project_id}")
|
|
205
|
+
logger.info(f" Version ID: {version_id}")
|
|
206
|
+
else:
|
|
207
|
+
logger.info(f"Using existing project: {project_id}")
|
|
208
|
+
version_info = client.get_project_version(project_id, version_id)
|
|
209
|
+
properties_id = version_info["propertiesId"]
|
|
210
|
+
|
|
211
|
+
# 7. Validate parameters
|
|
212
|
+
validate_parameters(
|
|
213
|
+
em_data, sources_info, wavelength, pixel_size,
|
|
214
|
+
threshold, max_iterations, boundary_width, is_vectorial
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# 8. Upload RI/permittivity
|
|
218
|
+
logger.info(f"Uploading electromagnetic data ({data_type})...")
|
|
219
|
+
if np.isrealobj(em_data):
|
|
220
|
+
binary_data_type = "float32"
|
|
221
|
+
else:
|
|
222
|
+
binary_data_type = "complex64"
|
|
223
|
+
|
|
224
|
+
filename = "refractive_index.bin" if data_type == "RefractiveIndex" else "permittivity.bin"
|
|
225
|
+
em_file_info = upload_array_as_binary_zipped(
|
|
226
|
+
client, project_id, version_id, em_data, filename, binary_data_type
|
|
227
|
+
)
|
|
228
|
+
em_data_id = _extract_file_id(em_file_info)
|
|
229
|
+
logger.info(f" Uploaded {data_type} (ID: {em_data_id})")
|
|
230
|
+
|
|
231
|
+
# 9. Upload sources
|
|
232
|
+
logger.info("Uploading input sources...")
|
|
233
|
+
source_ids = []
|
|
234
|
+
for i, src in enumerate(sources_info):
|
|
235
|
+
src_filename = f"source{i}.bin"
|
|
236
|
+
src_file_info = upload_source_field_as_binary_zipped(
|
|
237
|
+
client, project_id, version_id, src["field"], src_filename, "complex64"
|
|
238
|
+
)
|
|
239
|
+
src_id = _extract_file_id(src_file_info)
|
|
240
|
+
source_ids.append(src_id)
|
|
241
|
+
logger.info(f" Source {i + 1} uploaded (ID: {src_id})")
|
|
242
|
+
|
|
243
|
+
# 10. Build WavesimProperties payload
|
|
244
|
+
logger.info("Building command payload...")
|
|
245
|
+
domain_x_um = float(nx) * float(pixel_size)
|
|
246
|
+
domain_y_um = float(ny) * float(pixel_size)
|
|
247
|
+
domain_z_um = float(nz) * float(pixel_size)
|
|
248
|
+
|
|
249
|
+
simulation_domain = create_simulation_domain(domain_x_um, domain_y_um, domain_z_um)
|
|
250
|
+
background_medium = create_medium("background", "Background", background_ri)
|
|
251
|
+
|
|
252
|
+
primitive_name = data_type.lower().replace(" ", "_") + "_map"
|
|
253
|
+
voxel_primitive = create_voxels_model_primitive(
|
|
254
|
+
primitive_name, em_data_id, "background",
|
|
255
|
+
0.0, 0.0, 0.0,
|
|
256
|
+
float(pixel_size), float(pixel_size), float(pixel_size),
|
|
257
|
+
int(nx), int(ny), int(nz),
|
|
258
|
+
"Complex64", data_type,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
input_sources = []
|
|
262
|
+
for i, src in enumerate(sources_info):
|
|
263
|
+
src_shape = src["field"].shape
|
|
264
|
+
while len(src_shape) < 3:
|
|
265
|
+
src_shape = src_shape + (1,)
|
|
266
|
+
src_nx, src_ny, src_nz = src_shape[0], src_shape[1], src_shape[2]
|
|
267
|
+
|
|
268
|
+
pol_num = src["polarization"]
|
|
269
|
+
pol_map = {0: "x", 1: "y", 2: "z"}
|
|
270
|
+
pol_str = pol_map.get(pol_num, "none")
|
|
271
|
+
|
|
272
|
+
custom_source = create_custom_field_source(
|
|
273
|
+
name=f"source_{i + 1}",
|
|
274
|
+
field_data_id=source_ids[i],
|
|
275
|
+
position_x=src["position"][0],
|
|
276
|
+
position_y=src["position"][1],
|
|
277
|
+
position_z=src["position"][2],
|
|
278
|
+
grid_nx=src_nx,
|
|
279
|
+
grid_ny=src_ny,
|
|
280
|
+
grid_nz=src_nz,
|
|
281
|
+
polarization=pol_str,
|
|
282
|
+
amplitude_real=1.0,
|
|
283
|
+
amplitude_imag=0.0,
|
|
284
|
+
data_format="Complex64",
|
|
285
|
+
origin="topleft",
|
|
286
|
+
)
|
|
287
|
+
input_sources.append(custom_source)
|
|
288
|
+
|
|
289
|
+
boundary_size_um = float(boundary_width) * float(pixel_size)
|
|
290
|
+
|
|
291
|
+
props = create_wavesim_properties(
|
|
292
|
+
simulation_domain=simulation_domain,
|
|
293
|
+
primitives=[voxel_primitive],
|
|
294
|
+
input_sources=input_sources,
|
|
295
|
+
mediums=[background_medium],
|
|
296
|
+
monitors=[],
|
|
297
|
+
background_medium_id="background",
|
|
298
|
+
wavelength=float(wavelength),
|
|
299
|
+
voxel_size=float(pixel_size),
|
|
300
|
+
boundary_size_um=boundary_size_um,
|
|
301
|
+
residual_norm_threshold=float(threshold),
|
|
302
|
+
residual_norm_max_iterations=int(max_iterations),
|
|
303
|
+
periodic_x=bool(periodic[0]),
|
|
304
|
+
periodic_y=bool(periodic[1]),
|
|
305
|
+
periodic_z=bool(periodic[2]),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
logger.info(f" Payload created with {len(sources_info)} source(s)")
|
|
309
|
+
|
|
310
|
+
# 11. Update properties
|
|
311
|
+
logger.info("Updating project properties...")
|
|
312
|
+
client.update_project_properties(properties_id, props)
|
|
313
|
+
|
|
314
|
+
# 12. Submit command
|
|
315
|
+
logger.info("Submitting simulation command...")
|
|
316
|
+
cmd_status = client.create_command(project_id, version_id, engine_preference, use_gpu)
|
|
317
|
+
command_id = cmd_status.get("commandId") or cmd_status.get("id")
|
|
318
|
+
if not command_id:
|
|
319
|
+
raise RuntimeError("No command ID found in response")
|
|
320
|
+
logger.info(f" Command ID: {command_id}")
|
|
321
|
+
|
|
322
|
+
# 13. Poll for completion
|
|
323
|
+
logger.info("Waiting for simulation to complete...")
|
|
324
|
+
start_time = time.time()
|
|
325
|
+
consecutive_errors = 0
|
|
326
|
+
max_consecutive_errors = 3
|
|
327
|
+
final_status = None
|
|
328
|
+
|
|
329
|
+
while True:
|
|
330
|
+
try:
|
|
331
|
+
status = client.get_command_status(command_id)
|
|
332
|
+
consecutive_errors = 0
|
|
333
|
+
except Exception as e:
|
|
334
|
+
consecutive_errors += 1
|
|
335
|
+
if consecutive_errors >= max_consecutive_errors:
|
|
336
|
+
raise RuntimeError(
|
|
337
|
+
f"Failed to get command status after {consecutive_errors} attempts. "
|
|
338
|
+
f"Last error: {e}. Command ID: {command_id}"
|
|
339
|
+
)
|
|
340
|
+
logger.warning(
|
|
341
|
+
f"Status check failed (attempt {consecutive_errors}/{max_consecutive_errors}): {e}. "
|
|
342
|
+
f"Retrying in {poll_interval}s..."
|
|
343
|
+
)
|
|
344
|
+
time.sleep(poll_interval)
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
elapsed = time.time() - start_time
|
|
348
|
+
status_str = status.get("status", "Unknown")
|
|
349
|
+
|
|
350
|
+
# Parse progress if available
|
|
351
|
+
progress_dto = status.get("progressDto")
|
|
352
|
+
if progress_dto:
|
|
353
|
+
progress_info = parse_progress_data(progress_dto)
|
|
354
|
+
if progress_callback:
|
|
355
|
+
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
|
+
else:
|
|
367
|
+
logger.info(f" [{elapsed:.0f}s] {status_str}")
|
|
368
|
+
|
|
369
|
+
if status_str in ("Completed", "Failed", "Cancelled"):
|
|
370
|
+
final_status = status
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
time.sleep(poll_interval)
|
|
374
|
+
|
|
375
|
+
total_time = time.time() - start_time
|
|
376
|
+
|
|
377
|
+
if final_status.get("status") != "Completed":
|
|
378
|
+
# Build detailed error message
|
|
379
|
+
error_parts = [f"Simulation ended with status: {final_status.get('status')}"]
|
|
380
|
+
for field in ("errorMessage", "ErrorMessage", "statusText", "StatusText"):
|
|
381
|
+
val = final_status.get(field)
|
|
382
|
+
if val:
|
|
383
|
+
error_parts.append(str(val))
|
|
384
|
+
progress_dto = final_status.get("progressDto") or final_status.get("progress") or final_status.get("Progress")
|
|
385
|
+
if progress_dto and isinstance(progress_dto, dict):
|
|
386
|
+
for field in ("progress", "Progress"):
|
|
387
|
+
val = progress_dto.get(field)
|
|
388
|
+
if val:
|
|
389
|
+
error_parts.append(f"Details: {val}")
|
|
390
|
+
data_field = progress_dto.get("data") or progress_dto.get("Data")
|
|
391
|
+
if data_field:
|
|
392
|
+
try:
|
|
393
|
+
if isinstance(data_field, str):
|
|
394
|
+
data_obj = json.loads(data_field)
|
|
395
|
+
else:
|
|
396
|
+
data_obj = data_field
|
|
397
|
+
if "residual_norm" in data_obj:
|
|
398
|
+
error_parts.append(f"Final residual: {data_obj['residual_norm']:.2e}")
|
|
399
|
+
if "error" in data_obj and data_obj["error"]:
|
|
400
|
+
error_parts.append(f"Error: {data_obj['error']}")
|
|
401
|
+
except (json.JSONDecodeError, TypeError):
|
|
402
|
+
error_parts.append(f"Data: {data_field}")
|
|
403
|
+
raise RuntimeError(". ".join(error_parts))
|
|
404
|
+
|
|
405
|
+
logger.info(f" Simulation completed in {total_time:.1f} seconds!")
|
|
406
|
+
|
|
407
|
+
# 14. Download output field
|
|
408
|
+
logger.info("Downloading results...")
|
|
409
|
+
server_info = client.get_server_info()
|
|
410
|
+
use_machine_service = server_info["uses_machine_service"]
|
|
411
|
+
|
|
412
|
+
output_file_id = None
|
|
413
|
+
|
|
414
|
+
if use_machine_service:
|
|
415
|
+
files = client.list_command_files(command_id)
|
|
416
|
+
logger.info(f" Found {len(files)} files from machine service")
|
|
417
|
+
for f in files:
|
|
418
|
+
fname = f.get("fileName") or f.get("originalFileName", "")
|
|
419
|
+
if fname.lower() == "output.bin":
|
|
420
|
+
output_file_id = f.get("blobName")
|
|
421
|
+
break
|
|
422
|
+
else:
|
|
423
|
+
version_info = client.get_project_version(project_id, version_id)
|
|
424
|
+
output_file_ids = version_info.get("outputFileIds", [])
|
|
425
|
+
logger.info(f" Found {len(output_file_ids)} output file IDs from cloud storage")
|
|
426
|
+
for fid in output_file_ids:
|
|
427
|
+
try:
|
|
428
|
+
preview = client.get_file_preview(project_id, version_id, fid)
|
|
429
|
+
fname = preview.get("fileName") or preview.get("originalFileName", "")
|
|
430
|
+
if fname.lower() == "output.bin":
|
|
431
|
+
output_file_id = fid # Use the original ID from outputFileIds
|
|
432
|
+
break
|
|
433
|
+
except Exception:
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
if not output_file_id:
|
|
437
|
+
raise RuntimeError("output.bin not found in command files")
|
|
438
|
+
|
|
439
|
+
logger.info(f" Found output.bin (ID: {output_file_id})")
|
|
440
|
+
|
|
441
|
+
# Download to memory
|
|
442
|
+
output_data = client.download_file_data(project_id, version_id, output_file_id)
|
|
443
|
+
logger.info(f" Downloaded {len(output_data) / (1024 * 1024):.2f} MB")
|
|
444
|
+
|
|
445
|
+
# 15. Parse output
|
|
446
|
+
logger.info("Reading output field...")
|
|
447
|
+
if is_vectorial:
|
|
448
|
+
dims = (3, nx, ny, nz)
|
|
449
|
+
else:
|
|
450
|
+
dims = (nx, ny, nz)
|
|
451
|
+
|
|
452
|
+
E = read_binary_field(output_data, dims, "Complex64")
|
|
453
|
+
logger.info(f" Output shape: {E.shape}")
|
|
454
|
+
|
|
455
|
+
converged = final_status.get("status") == "Completed"
|
|
456
|
+
|
|
457
|
+
logger.info("=" * 60)
|
|
458
|
+
logger.info("Simulation completed successfully!")
|
|
459
|
+
logger.info("=" * 60)
|
|
460
|
+
|
|
461
|
+
return E, converged
|