goad-py 0.3.0__cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl → 0.4.2__cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
Potentially problematic release.
This version of goad-py might be problematic. Click here for more details.
- goad_py/__init__.py +6 -4
- goad_py/_goad_py.abi3.so +0 -0
- goad_py/convergence.py +382 -0
- goad_py/{__init__.pyi → goad_py.pyi} +124 -16
- {goad_py-0.3.0.dist-info → goad_py-0.4.2.dist-info}/METADATA +32 -7
- goad_py-0.4.2.dist-info/RECORD +7 -0
- {goad_py-0.3.0.dist-info → goad_py-0.4.2.dist-info}/WHEEL +1 -1
- goad_py/goad_py.abi3.so +0 -0
- goad_py/py.typed +0 -0
- goad_py-0.3.0.dist-info/RECORD +0 -7
goad_py/__init__.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
from
|
|
1
|
+
# Re-export everything from the compiled Rust module
|
|
2
|
+
from goad_py._goad_py import *
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# Import Python modules
|
|
5
|
+
from .convergence import Convergence, Convergable, ConvergenceResults
|
|
6
|
+
|
|
7
|
+
__all__ = ['Convergence', 'Convergable', 'ConvergenceResults']
|
goad_py/_goad_py.abi3.so
ADDED
|
Binary file
|
goad_py/convergence.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Dict, Optional, Tuple
|
|
3
|
+
import numpy as np
|
|
4
|
+
from . import _goad_py as goad
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Convergable:
|
|
9
|
+
"""Represents a variable to monitor for convergence."""
|
|
10
|
+
variable: str # 'asymmetry', 'scatt', 'ext', or 'albedo'
|
|
11
|
+
tolerance_type: str = 'relative' # 'relative' or 'absolute'
|
|
12
|
+
tolerance: float = 0.01
|
|
13
|
+
|
|
14
|
+
def __post_init__(self):
|
|
15
|
+
valid_variables = {'asymmetry', 'scatt', 'ext', 'albedo'}
|
|
16
|
+
if self.variable not in valid_variables:
|
|
17
|
+
raise ValueError(f"Invalid variable '{self.variable}'. Must be one of {valid_variables}")
|
|
18
|
+
|
|
19
|
+
valid_types = {'relative', 'absolute'}
|
|
20
|
+
if self.tolerance_type not in valid_types:
|
|
21
|
+
raise ValueError(f"Invalid tolerance_type '{self.tolerance_type}'. Must be one of {valid_types}")
|
|
22
|
+
|
|
23
|
+
if self.tolerance <= 0:
|
|
24
|
+
raise ValueError(f"Tolerance must be positive, got {self.tolerance}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ConvergenceResults:
|
|
29
|
+
"""Results from a convergence study."""
|
|
30
|
+
converged: bool
|
|
31
|
+
n_orientations: int
|
|
32
|
+
values: Dict[str, float] # Final mean values for each tracked variable
|
|
33
|
+
sem_values: Dict[str, float] # Final SEM values for each tracked variable
|
|
34
|
+
mueller_1d: Optional[np.ndarray] = None
|
|
35
|
+
mueller_2d: Optional[np.ndarray] = None
|
|
36
|
+
convergence_history: List[Tuple[int, str, float]] = None # (n_orientations, variable, sem)
|
|
37
|
+
warning: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Convergence:
|
|
41
|
+
"""Runs multiple MultiProblems until convergence criteria are met."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
settings: goad.Settings,
|
|
46
|
+
convergables: List[Convergable],
|
|
47
|
+
batch_size: int = 24,
|
|
48
|
+
max_orientations: int = 100_000,
|
|
49
|
+
min_batches: int = 10,
|
|
50
|
+
mueller_1d: bool = True,
|
|
51
|
+
mueller_2d: bool = False
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Initialize a convergence study.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
settings: GOAD settings for the simulation
|
|
58
|
+
convergables: List of variables to monitor for convergence
|
|
59
|
+
batch_size: Number of orientations per iteration
|
|
60
|
+
max_orientations: Maximum total orientations before stopping
|
|
61
|
+
min_batches: Minimum number of batches before allowing convergence
|
|
62
|
+
mueller_1d: Whether to collect 1D Mueller matrices
|
|
63
|
+
mueller_2d: Whether to collect 2D Mueller matrices
|
|
64
|
+
"""
|
|
65
|
+
self.settings = settings
|
|
66
|
+
self.convergables = convergables
|
|
67
|
+
self.batch_size = batch_size
|
|
68
|
+
self.max_orientations = max_orientations
|
|
69
|
+
self.min_batches = min_batches
|
|
70
|
+
self.mueller_1d = mueller_1d
|
|
71
|
+
self.mueller_2d = mueller_2d
|
|
72
|
+
|
|
73
|
+
# Validate inputs
|
|
74
|
+
if not convergables:
|
|
75
|
+
raise ValueError("Must specify at least one convergable")
|
|
76
|
+
|
|
77
|
+
if batch_size <= 0:
|
|
78
|
+
raise ValueError(f"batch_size must be positive, got {batch_size}")
|
|
79
|
+
|
|
80
|
+
if max_orientations <= 0:
|
|
81
|
+
raise ValueError(f"max_orientations must be positive, got {max_orientations}")
|
|
82
|
+
|
|
83
|
+
if min_batches <= 0:
|
|
84
|
+
raise ValueError(f"min_batches must be positive, got {min_batches}")
|
|
85
|
+
|
|
86
|
+
# Initialize tracking variables
|
|
87
|
+
self.n_orientations = 0
|
|
88
|
+
self.convergence_history = []
|
|
89
|
+
|
|
90
|
+
# Batch-based statistics tracking for rigorous SEM calculation
|
|
91
|
+
self.batch_data = [] # List of batch statistics
|
|
92
|
+
|
|
93
|
+
# Mueller matrix accumulation
|
|
94
|
+
self.mueller_1d_sum = None
|
|
95
|
+
self.mueller_2d_sum = None
|
|
96
|
+
|
|
97
|
+
def _update_statistics(self, results: goad.Results, batch_size: int):
|
|
98
|
+
"""Update statistics with new batch results.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
results: Results from a MultiProblem run (pre-averaged over batch_size orientations)
|
|
102
|
+
batch_size: Number of orientations in this batch
|
|
103
|
+
"""
|
|
104
|
+
# Check for None values indicating Custom binning
|
|
105
|
+
if (results.asymmetry is None or results.scat_cross is None or
|
|
106
|
+
results.ext_cross is None or results.albedo is None):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"Received None values for integrated properties. "
|
|
109
|
+
"This likely means Custom binning scheme is being used. "
|
|
110
|
+
"Convergence requires Simple or Interval binning schemes."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Store batch data for proper statistical analysis
|
|
114
|
+
batch_info = {
|
|
115
|
+
'batch_size': batch_size,
|
|
116
|
+
'values': {},
|
|
117
|
+
'weights': {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Store values and weights for tracked variables
|
|
121
|
+
for conv in self.convergables:
|
|
122
|
+
if conv.variable == 'asymmetry':
|
|
123
|
+
batch_info['values']['asymmetry'] = results.asymmetry
|
|
124
|
+
batch_info['weights']['asymmetry'] = results.scat_cross
|
|
125
|
+
elif conv.variable == 'scatt':
|
|
126
|
+
batch_info['values']['scatt'] = results.scat_cross
|
|
127
|
+
batch_info['weights']['scatt'] = 1.0 # Equal weighting
|
|
128
|
+
elif conv.variable == 'ext':
|
|
129
|
+
batch_info['values']['ext'] = results.ext_cross
|
|
130
|
+
batch_info['weights']['ext'] = 1.0 # Equal weighting
|
|
131
|
+
elif conv.variable == 'albedo':
|
|
132
|
+
batch_info['values']['albedo'] = results.albedo
|
|
133
|
+
batch_info['weights']['albedo'] = results.ext_cross + results.scat_cross
|
|
134
|
+
|
|
135
|
+
self.batch_data.append(batch_info)
|
|
136
|
+
|
|
137
|
+
# Update Mueller matrices if enabled
|
|
138
|
+
if self.mueller_1d and results.mueller_1d is not None:
|
|
139
|
+
mueller_1d_array = np.array(results.mueller_1d)
|
|
140
|
+
if self.mueller_1d_sum is None:
|
|
141
|
+
self.mueller_1d_sum = mueller_1d_array * batch_size
|
|
142
|
+
else:
|
|
143
|
+
self.mueller_1d_sum += mueller_1d_array * batch_size
|
|
144
|
+
|
|
145
|
+
if self.mueller_2d and results.mueller is not None:
|
|
146
|
+
mueller_2d_array = np.array(results.mueller)
|
|
147
|
+
if self.mueller_2d_sum is None:
|
|
148
|
+
self.mueller_2d_sum = mueller_2d_array * batch_size
|
|
149
|
+
else:
|
|
150
|
+
self.mueller_2d_sum += mueller_2d_array * batch_size
|
|
151
|
+
|
|
152
|
+
# Update total orientation count
|
|
153
|
+
self.n_orientations += batch_size
|
|
154
|
+
|
|
155
|
+
def _calculate_mean_and_sem(self, variable: str) -> Tuple[float, float]:
|
|
156
|
+
"""Calculate mean and standard error of the mean for a variable using batch data.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
variable: Variable name
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Tuple of (mean, sem)
|
|
163
|
+
"""
|
|
164
|
+
if not self.batch_data:
|
|
165
|
+
return 0.0, float('inf')
|
|
166
|
+
|
|
167
|
+
# Extract batch values and weights
|
|
168
|
+
batch_values = []
|
|
169
|
+
batch_weights = []
|
|
170
|
+
batch_sizes = []
|
|
171
|
+
|
|
172
|
+
for batch in self.batch_data:
|
|
173
|
+
if variable in batch['values']:
|
|
174
|
+
batch_values.append(batch['values'][variable])
|
|
175
|
+
batch_weights.append(batch['weights'][variable])
|
|
176
|
+
batch_sizes.append(batch['batch_size'])
|
|
177
|
+
|
|
178
|
+
if not batch_values:
|
|
179
|
+
return 0.0, float('inf')
|
|
180
|
+
|
|
181
|
+
batch_values = np.array(batch_values)
|
|
182
|
+
batch_weights = np.array(batch_weights)
|
|
183
|
+
batch_sizes = np.array(batch_sizes)
|
|
184
|
+
|
|
185
|
+
# For weighted variables (asymmetry, albedo), use weighted statistics
|
|
186
|
+
if variable in ['asymmetry', 'albedo']:
|
|
187
|
+
# Calculate weighted mean across batches
|
|
188
|
+
# Each batch contributes: weight * batch_size * value
|
|
189
|
+
total_weighted_sum = np.sum(batch_weights * batch_sizes * batch_values)
|
|
190
|
+
total_weight = np.sum(batch_weights * batch_sizes)
|
|
191
|
+
weighted_mean = total_weighted_sum / total_weight
|
|
192
|
+
|
|
193
|
+
# Calculate weighted variance between batches
|
|
194
|
+
if len(batch_values) < 2:
|
|
195
|
+
return weighted_mean, float('inf') # Cannot estimate variance with < 2 batches
|
|
196
|
+
|
|
197
|
+
# For batch means, we need to account for the effective weight of each batch
|
|
198
|
+
effective_weights = batch_weights * batch_sizes
|
|
199
|
+
weighted_variance_batch_means = np.sum(effective_weights * (batch_values - weighted_mean)**2) / np.sum(effective_weights)
|
|
200
|
+
|
|
201
|
+
# Scale up to estimate population variance
|
|
202
|
+
# Batch means have variance = population_variance / average_batch_size
|
|
203
|
+
# So population_variance ≈ batch_means_variance * average_batch_size
|
|
204
|
+
avg_batch_size = np.average(batch_sizes, weights=effective_weights)
|
|
205
|
+
estimated_population_variance = weighted_variance_batch_means * avg_batch_size
|
|
206
|
+
|
|
207
|
+
# Calculate SEM for the total sample (using n-1 for sample standard error)
|
|
208
|
+
total_n = np.sum(batch_sizes)
|
|
209
|
+
sem = np.sqrt(estimated_population_variance / (total_n - 1))
|
|
210
|
+
|
|
211
|
+
return weighted_mean, sem
|
|
212
|
+
|
|
213
|
+
else:
|
|
214
|
+
# For unweighted variables (scatt, ext), use simple batch statistics
|
|
215
|
+
# Calculate mean of batch means, weighted by batch size
|
|
216
|
+
total_sum = np.sum(batch_sizes * batch_values)
|
|
217
|
+
total_n = np.sum(batch_sizes)
|
|
218
|
+
mean = total_sum / total_n
|
|
219
|
+
|
|
220
|
+
# Calculate variance between batch means
|
|
221
|
+
if len(batch_values) < 2:
|
|
222
|
+
return mean, float('inf')
|
|
223
|
+
|
|
224
|
+
batch_means_variance = np.var(batch_values, ddof=1)
|
|
225
|
+
|
|
226
|
+
# Scale up to estimate population variance
|
|
227
|
+
# Batch means have variance = population_variance / average_batch_size
|
|
228
|
+
# So population_variance ≈ batch_means_variance * average_batch_size
|
|
229
|
+
avg_batch_size = np.mean(batch_sizes)
|
|
230
|
+
estimated_population_variance = batch_means_variance * avg_batch_size
|
|
231
|
+
|
|
232
|
+
# Calculate SEM for the total sample (using n-1 for sample standard error)
|
|
233
|
+
sem = np.sqrt(estimated_population_variance / (total_n - 1))
|
|
234
|
+
|
|
235
|
+
return mean, sem
|
|
236
|
+
|
|
237
|
+
def _check_convergence(self) -> Dict[str, bool]:
|
|
238
|
+
"""Check if all convergence criteria are met.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Dict mapping variable names to convergence status
|
|
242
|
+
"""
|
|
243
|
+
converged = {}
|
|
244
|
+
|
|
245
|
+
for conv in self.convergables:
|
|
246
|
+
mean, sem = self._calculate_mean_and_sem(conv.variable)
|
|
247
|
+
|
|
248
|
+
# Calculate tolerance based on type
|
|
249
|
+
if conv.tolerance_type == 'relative':
|
|
250
|
+
# Relative tolerance: SEM / |mean| < tolerance
|
|
251
|
+
if mean != 0:
|
|
252
|
+
relative_sem = sem / abs(mean)
|
|
253
|
+
converged[conv.variable] = relative_sem < conv.tolerance
|
|
254
|
+
else:
|
|
255
|
+
# If mean is zero, use absolute comparison
|
|
256
|
+
converged[conv.variable] = sem < conv.tolerance
|
|
257
|
+
else:
|
|
258
|
+
# Absolute tolerance: SEM < tolerance
|
|
259
|
+
converged[conv.variable] = sem < conv.tolerance
|
|
260
|
+
|
|
261
|
+
return converged
|
|
262
|
+
|
|
263
|
+
def _all_converged(self) -> bool:
|
|
264
|
+
"""Check if all variables have converged.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
True if all variables meet their convergence criteria and minimum batches completed
|
|
268
|
+
"""
|
|
269
|
+
# Check minimum batches requirement first
|
|
270
|
+
if len(self.batch_data) < self.min_batches:
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
converged_status = self._check_convergence()
|
|
274
|
+
return all(converged_status.values())
|
|
275
|
+
|
|
276
|
+
def _print_progress(self, iteration: int):
|
|
277
|
+
"""Print convergence progress.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
iteration: Current iteration number
|
|
281
|
+
"""
|
|
282
|
+
print(f"\nIteration {iteration} ({self.n_orientations} orientations):")
|
|
283
|
+
|
|
284
|
+
converged_status = self._check_convergence()
|
|
285
|
+
|
|
286
|
+
for conv in self.convergables:
|
|
287
|
+
mean, sem = self._calculate_mean_and_sem(conv.variable)
|
|
288
|
+
|
|
289
|
+
# Calculate 95% CI
|
|
290
|
+
ci_lower = mean - 1.96 * sem
|
|
291
|
+
ci_upper = mean + 1.96 * sem
|
|
292
|
+
|
|
293
|
+
# Format based on tolerance type
|
|
294
|
+
if conv.tolerance_type == 'relative':
|
|
295
|
+
if mean != 0:
|
|
296
|
+
relative_sem = sem / abs(mean)
|
|
297
|
+
target_str = f"{conv.tolerance*100:.1f}%"
|
|
298
|
+
current_str = f"{relative_sem*100:.2f}%"
|
|
299
|
+
else:
|
|
300
|
+
target_str = f"{conv.tolerance} (abs, mean=0)"
|
|
301
|
+
current_str = f"{sem:.4g}"
|
|
302
|
+
else:
|
|
303
|
+
target_str = f"{conv.tolerance}"
|
|
304
|
+
current_str = f"{sem:.4g}"
|
|
305
|
+
|
|
306
|
+
# Status indicator
|
|
307
|
+
status = "✓" if converged_status[conv.variable] else "❌"
|
|
308
|
+
|
|
309
|
+
# Print line with mean, SEM, CI, and convergence status
|
|
310
|
+
print(f" {conv.variable:<10}: {mean:.6f} ± {sem:.6f} [{ci_lower:.6f}, {ci_upper:.6f}] | SEM: {current_str} (target: {target_str}) {status}")
|
|
311
|
+
|
|
312
|
+
# Add to convergence history
|
|
313
|
+
self.convergence_history.append((self.n_orientations, conv.variable, sem))
|
|
314
|
+
|
|
315
|
+
def run(self) -> ConvergenceResults:
|
|
316
|
+
"""Run the convergence study.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
ConvergenceResults containing final values and convergence status
|
|
320
|
+
"""
|
|
321
|
+
iteration = 0
|
|
322
|
+
converged = False
|
|
323
|
+
warning = None
|
|
324
|
+
|
|
325
|
+
while not converged and self.n_orientations < self.max_orientations:
|
|
326
|
+
iteration += 1
|
|
327
|
+
|
|
328
|
+
# Determine batch size for this iteration
|
|
329
|
+
remaining = self.max_orientations - self.n_orientations
|
|
330
|
+
batch_size = min(self.batch_size, remaining)
|
|
331
|
+
|
|
332
|
+
# Set batch size
|
|
333
|
+
orientations = goad.create_uniform_orientation(batch_size)
|
|
334
|
+
|
|
335
|
+
# Set the orientations for the settings
|
|
336
|
+
self.settings.orientation = orientations
|
|
337
|
+
|
|
338
|
+
mp = goad.MultiProblem(self.settings)
|
|
339
|
+
mp.py_solve()
|
|
340
|
+
|
|
341
|
+
# Update statistics
|
|
342
|
+
self._update_statistics(mp.results, batch_size)
|
|
343
|
+
|
|
344
|
+
# Print progress
|
|
345
|
+
self._print_progress(iteration)
|
|
346
|
+
|
|
347
|
+
# Check convergence
|
|
348
|
+
converged = self._all_converged()
|
|
349
|
+
|
|
350
|
+
# Prepare final results
|
|
351
|
+
if converged:
|
|
352
|
+
print(f"\nConverged after {self.n_orientations} orientations.")
|
|
353
|
+
else:
|
|
354
|
+
warning = f"Maximum orientations ({self.max_orientations}) reached without convergence"
|
|
355
|
+
print(f"\nWarning: {warning}")
|
|
356
|
+
|
|
357
|
+
# Calculate final values and SEMs
|
|
358
|
+
final_values = {}
|
|
359
|
+
final_sems = {}
|
|
360
|
+
for conv in self.convergables:
|
|
361
|
+
mean, sem = self._calculate_mean_and_sem(conv.variable)
|
|
362
|
+
final_values[conv.variable] = mean
|
|
363
|
+
final_sems[conv.variable] = sem
|
|
364
|
+
|
|
365
|
+
# Prepare Mueller matrices
|
|
366
|
+
mueller_1d = None
|
|
367
|
+
mueller_2d = None
|
|
368
|
+
if self.mueller_1d and self.mueller_1d_sum is not None:
|
|
369
|
+
mueller_1d = self.mueller_1d_sum / self.n_orientations
|
|
370
|
+
if self.mueller_2d and self.mueller_2d_sum is not None:
|
|
371
|
+
mueller_2d = self.mueller_2d_sum / self.n_orientations
|
|
372
|
+
|
|
373
|
+
return ConvergenceResults(
|
|
374
|
+
converged=converged,
|
|
375
|
+
n_orientations=self.n_orientations,
|
|
376
|
+
values=final_values,
|
|
377
|
+
sem_values=final_sems,
|
|
378
|
+
mueller_1d=mueller_1d,
|
|
379
|
+
mueller_2d=mueller_2d,
|
|
380
|
+
convergence_history=self.convergence_history,
|
|
381
|
+
warning=warning
|
|
382
|
+
)
|
|
@@ -20,27 +20,18 @@ Default behavior (minimal setup):
|
|
|
20
20
|
Example (minimal setup):
|
|
21
21
|
import goad_py as goad
|
|
22
22
|
|
|
23
|
-
# Ultra-simple single orientation
|
|
24
23
|
settings = goad.Settings("particle.obj")
|
|
25
|
-
problem = goad.Problem(settings)
|
|
26
|
-
problem.py_solve()
|
|
27
|
-
print(f"Extinction: {problem.results.ext_cross}")
|
|
28
|
-
|
|
29
|
-
# Multi-orientation averaging (100 random orientations)
|
|
30
|
-
orientations = goad.create_uniform_orientation(100)
|
|
31
|
-
settings = goad.Settings("particle.obj", orientation=orientations)
|
|
32
24
|
mp = goad.MultiProblem(settings)
|
|
33
25
|
mp.py_solve()
|
|
34
|
-
print(f"Average extinction: {mp.results.ext_cross}")
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
print(f"Results: {mp.results.scat_cross}")
|
|
27
|
+
results = mp.results
|
|
28
|
+
print(f"Scattering cross-section: {results.scat_cross}")
|
|
29
|
+
print(f"Extinction cross-section: {results.ext_cross}")
|
|
30
|
+
print(f"Asymmetry parameter: {results.asymmetry}")
|
|
41
31
|
"""
|
|
42
32
|
|
|
43
|
-
from typing import Optional, List, Dict, Any, Union
|
|
33
|
+
from typing import Optional, List, Dict, Any, Union, Tuple
|
|
34
|
+
import numpy as np
|
|
44
35
|
|
|
45
36
|
class Euler:
|
|
46
37
|
"""Euler angles for rotations."""
|
|
@@ -342,4 +333,121 @@ def create_discrete_orientation(eulers: List[Euler], euler_convention: Optional[
|
|
|
342
333
|
|
|
343
334
|
def sum_as_string(a: int, b: int) -> str: ...
|
|
344
335
|
|
|
345
|
-
def goad_py_add() -> None: ...
|
|
336
|
+
def goad_py_add() -> None: ...
|
|
337
|
+
|
|
338
|
+
# Convergence Analysis Classes
|
|
339
|
+
|
|
340
|
+
class Convergable:
|
|
341
|
+
"""Represents a variable to monitor for convergence.
|
|
342
|
+
|
|
343
|
+
Defines convergence criteria for integrated scattering parameters
|
|
344
|
+
including asymmetry parameter, scattering cross-section, extinction
|
|
345
|
+
cross-section, and single-scattering albedo.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
variable: str
|
|
349
|
+
tolerance_type: str
|
|
350
|
+
tolerance: float
|
|
351
|
+
|
|
352
|
+
def __init__(
|
|
353
|
+
self,
|
|
354
|
+
variable: str,
|
|
355
|
+
tolerance_type: str = 'relative',
|
|
356
|
+
tolerance: float = 0.01
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Initialize convergence criterion.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
variable: Variable to monitor ('asymmetry', 'scatt', 'ext', 'albedo')
|
|
362
|
+
tolerance_type: 'relative' or 'absolute' tolerance
|
|
363
|
+
tolerance: Tolerance value (relative as fraction, absolute as value)
|
|
364
|
+
|
|
365
|
+
Raises:
|
|
366
|
+
ValueError: If variable name or tolerance_type is invalid
|
|
367
|
+
"""
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
class ConvergenceResults:
|
|
371
|
+
"""Results from a convergence study.
|
|
372
|
+
|
|
373
|
+
Contains final convergence status, parameter values with uncertainties,
|
|
374
|
+
and complete convergence history for analysis.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
converged: bool
|
|
378
|
+
n_orientations: int
|
|
379
|
+
values: Dict[str, float]
|
|
380
|
+
sem_values: Dict[str, float]
|
|
381
|
+
mueller_1d: Optional[np.ndarray]
|
|
382
|
+
mueller_2d: Optional[np.ndarray]
|
|
383
|
+
convergence_history: List[Tuple[int, str, float]]
|
|
384
|
+
warning: Optional[str]
|
|
385
|
+
|
|
386
|
+
def __init__(
|
|
387
|
+
self,
|
|
388
|
+
converged: bool,
|
|
389
|
+
n_orientations: int,
|
|
390
|
+
values: Dict[str, float],
|
|
391
|
+
sem_values: Dict[str, float],
|
|
392
|
+
mueller_1d: Optional[np.ndarray] = None,
|
|
393
|
+
mueller_2d: Optional[np.ndarray] = None,
|
|
394
|
+
convergence_history: List[Tuple[int, str, float]] = None,
|
|
395
|
+
warning: Optional[str] = None
|
|
396
|
+
) -> None: ...
|
|
397
|
+
|
|
398
|
+
class Convergence:
|
|
399
|
+
"""Runs multiple MultiProblems until convergence criteria are met.
|
|
400
|
+
|
|
401
|
+
Implements statistical convergence analysis for scattering parameters
|
|
402
|
+
using batch-based standard error estimation. Monitors multiple variables
|
|
403
|
+
simultaneously and stops when all meet their convergence criteria.
|
|
404
|
+
|
|
405
|
+
Example:
|
|
406
|
+
convergence = Convergence(
|
|
407
|
+
settings=goad.Settings("particle.obj"),
|
|
408
|
+
convergables=[
|
|
409
|
+
Convergable('asymmetry', 'absolute', 0.005),
|
|
410
|
+
Convergable('scatt', 'relative', 0.01),
|
|
411
|
+
],
|
|
412
|
+
batch_size=100
|
|
413
|
+
)
|
|
414
|
+
results = convergence.run()
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
def __init__(
|
|
418
|
+
self,
|
|
419
|
+
settings: Settings,
|
|
420
|
+
convergables: List[Convergable],
|
|
421
|
+
batch_size: int = 24,
|
|
422
|
+
max_orientations: int = 100_000,
|
|
423
|
+
min_batches: int = 10,
|
|
424
|
+
mueller_1d: bool = True,
|
|
425
|
+
mueller_2d: bool = False
|
|
426
|
+
) -> None:
|
|
427
|
+
"""Initialize convergence study.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
settings: GOAD settings for the simulation
|
|
431
|
+
convergables: List of variables to monitor for convergence
|
|
432
|
+
batch_size: Number of orientations per iteration
|
|
433
|
+
max_orientations: Maximum total orientations before stopping
|
|
434
|
+
min_batches: Minimum number of batches before allowing convergence
|
|
435
|
+
mueller_1d: Whether to collect 1D Mueller matrices
|
|
436
|
+
mueller_2d: Whether to collect 2D Mueller matrices
|
|
437
|
+
|
|
438
|
+
Raises:
|
|
439
|
+
ValueError: If parameters are invalid or no convergables specified
|
|
440
|
+
"""
|
|
441
|
+
...
|
|
442
|
+
|
|
443
|
+
def run(self) -> ConvergenceResults:
|
|
444
|
+
"""Run the convergence study.
|
|
445
|
+
|
|
446
|
+
Executes batches of orientations until all convergence criteria
|
|
447
|
+
are met or maximum orientations reached. Provides progress updates
|
|
448
|
+
and rigorous statistical analysis.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
ConvergenceResults containing final values and convergence status
|
|
452
|
+
"""
|
|
453
|
+
...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: goad-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
@@ -43,14 +43,38 @@ pip install goad-py
|
|
|
43
43
|
import goad_py
|
|
44
44
|
|
|
45
45
|
# Create a problem with minimal setup
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
results = problem.py_solve()
|
|
46
|
+
settings = goad_py.Settings("path/to/geometry.obj")
|
|
47
|
+
mp = goad_py.MultiProblem(settings)
|
|
48
|
+
mp.py_solve()
|
|
50
49
|
|
|
51
50
|
# Access scattering data
|
|
52
|
-
|
|
53
|
-
print(f"Scattering cross
|
|
51
|
+
results = mp.results
|
|
52
|
+
print(f"Scattering cross-section: {results.scat_cross}")
|
|
53
|
+
print(f"Extinction cross-section: {results.ext_cross}")
|
|
54
|
+
print(f"Asymmetry parameter: {results.asymmetry}")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Convergence Analysis
|
|
58
|
+
|
|
59
|
+
For statistical error estimation, use the convergence analysis functionality:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from goad_py import Convergence, Convergable
|
|
63
|
+
|
|
64
|
+
# Set up convergence analysis
|
|
65
|
+
convergence = Convergence(
|
|
66
|
+
settings=goad_py.Settings(geom_path="path/to/geometry.obj"),
|
|
67
|
+
convergables=[
|
|
68
|
+
Convergable('asymmetry', 'absolute', 0.005), # absolute SEM < 0.005
|
|
69
|
+
Convergable('scatt', 'relative', 0.01), # relative SEM < 1%
|
|
70
|
+
],
|
|
71
|
+
batch_size=100
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Run until convergence
|
|
75
|
+
results = convergence.run()
|
|
76
|
+
print(f"Converged: {results.converged}")
|
|
77
|
+
print(f"Final values: {results.values}")
|
|
54
78
|
```
|
|
55
79
|
|
|
56
80
|
## Features
|
|
@@ -59,6 +83,7 @@ print(f"Scattering cross section: {results.sca_cross_section}")
|
|
|
59
83
|
- Support for various 3D geometry formats
|
|
60
84
|
- Configurable wavelength, refractive index, and orientations
|
|
61
85
|
- Multi-orientation averaging capabilities
|
|
86
|
+
- Convergence analysis for statistical error estimation
|
|
62
87
|
- Efficient parallel computation with GIL release
|
|
63
88
|
|
|
64
89
|
## Documentation
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
goad_py-0.4.2.dist-info/METADATA,sha256=JWxD4wctGOJzIWAWzjEpHPrdKbEgabqBjqGbgAptSR4,3181
|
|
2
|
+
goad_py-0.4.2.dist-info/WHEEL,sha256=Fxtqj9cyhJGORzVvbvofQXkYWBSW4MjI7J5bcuB1MfI,129
|
|
3
|
+
goad_py/__init__.py,sha256=vZn5zZZdTgm1qTQFYCqgNKBKDAoheMo5OvwdLIIl3Dk,242
|
|
4
|
+
goad_py/_goad_py.abi3.so,sha256=rv6AZNuS5kQhGXNa1ChNm-Oxobrd9ErO9EFScr7To3k,2041904
|
|
5
|
+
goad_py/convergence.py,sha256=8HTlrpv3RLD8FfmZuT_2mySVtnpHaPLKx6Rcw-jDgB0,14930
|
|
6
|
+
goad_py/goad_py.pyi,sha256=Ue33wS-DLeI2JTwpKyPXNk4D-Trt9S5MBQSO8qW5XGc,14922
|
|
7
|
+
goad_py-0.4.2.dist-info/RECORD,,
|
goad_py/goad_py.abi3.so
DELETED
|
Binary file
|
goad_py/py.typed
DELETED
|
File without changes
|
goad_py-0.3.0.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
goad_py-0.3.0.dist-info/METADATA,sha256=_QAoI_pML6HmYtVhYwEzjAjrZ1ieYWmib0Hk0QRvDGI,2434
|
|
2
|
-
goad_py-0.3.0.dist-info/WHEEL,sha256=9u0jSXl_m-TIQSYFMWCr0jbsFV_asy5EUcTos8UaOwU,129
|
|
3
|
-
goad_py/__init__.py,sha256=YW_7ejLyMUb8grQQmBo-vpbgTYF2xRruw6x7UxAerIg,111
|
|
4
|
-
goad_py/__init__.pyi,sha256=Fc4JBBbKMYZ0MA_DzycbC2v19YARzGKNg0bfmTkNhPQ,11489
|
|
5
|
-
goad_py/goad_py.abi3.so,sha256=V8lxf7aGsxof5Mmi54KZ06yFIxAp9N6iPzJMeXb9OGk,1951808
|
|
6
|
-
goad_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
goad_py-0.3.0.dist-info/RECORD,,
|