goad-py 0.6.0__cp38-abi3-musllinux_1_2_aarch64.whl → 0.7.0__cp38-abi3-musllinux_1_2_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/_goad_py.abi3.so +0 -0
- goad_py/convergence.py +233 -285
- goad_py/convergence_display.py +499 -0
- goad_py/phips_convergence.py +220 -192
- goad_py/unified_convergence.py +18 -18
- {goad_py-0.6.0.dist-info → goad_py-0.7.0.dist-info}/METADATA +2 -1
- goad_py-0.7.0.dist-info/RECORD +12 -0
- goad_py-0.6.0.dist-info/RECORD +0 -11
- {goad_py-0.6.0.dist-info → goad_py-0.7.0.dist-info}/WHEEL +0 -0
goad_py/_goad_py.abi3.so
CHANGED
|
Binary file
|
goad_py/convergence.py
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
from typing import List, Dict, Optional, Tuple
|
|
3
|
-
import numpy as np
|
|
4
|
-
from . import _goad_py as goad
|
|
1
|
+
import contextlib
|
|
5
2
|
import os
|
|
6
3
|
import random
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
7
6
|
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from . import _goad_py as goad
|
|
13
|
+
from .convergence_display import (
|
|
14
|
+
ArrayConvergenceVariable,
|
|
15
|
+
ConvergenceDisplay,
|
|
16
|
+
ConvergenceVariable,
|
|
17
|
+
)
|
|
8
18
|
|
|
9
19
|
|
|
10
20
|
@dataclass
|
|
@@ -96,6 +106,8 @@ class Convergence:
|
|
|
96
106
|
mueller_2d: Whether to collect 2D Mueller matrices
|
|
97
107
|
"""
|
|
98
108
|
self.settings = settings
|
|
109
|
+
# Enable quiet mode to suppress Rust progress bars
|
|
110
|
+
self.settings.quiet = True
|
|
99
111
|
self.convergables = convergables
|
|
100
112
|
self.batch_size = batch_size
|
|
101
113
|
self.max_orientations = max_orientations
|
|
@@ -129,6 +141,39 @@ class Convergence:
|
|
|
129
141
|
self.mueller_1d_sum = None
|
|
130
142
|
self.mueller_2d_sum = None
|
|
131
143
|
|
|
144
|
+
# Rich console
|
|
145
|
+
self._console = Console()
|
|
146
|
+
|
|
147
|
+
# Create display variables for the new display system
|
|
148
|
+
display_variables = []
|
|
149
|
+
for conv in self.convergables:
|
|
150
|
+
if conv.is_mueller():
|
|
151
|
+
display_variables.append(
|
|
152
|
+
ArrayConvergenceVariable(
|
|
153
|
+
name=conv.variable,
|
|
154
|
+
tolerance=conv.tolerance,
|
|
155
|
+
tolerance_type=conv.tolerance_type,
|
|
156
|
+
indices=conv.theta_indices,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
display_variables.append(
|
|
161
|
+
ConvergenceVariable(
|
|
162
|
+
name=conv.variable,
|
|
163
|
+
tolerance=conv.tolerance,
|
|
164
|
+
tolerance_type=conv.tolerance_type,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Initialize display system
|
|
169
|
+
self._display = ConvergenceDisplay(
|
|
170
|
+
variables=display_variables,
|
|
171
|
+
batch_size=self.batch_size,
|
|
172
|
+
min_batches=self.min_batches,
|
|
173
|
+
convergence_type=self._get_convergence_type(),
|
|
174
|
+
console=self._console,
|
|
175
|
+
)
|
|
176
|
+
|
|
132
177
|
def _update_statistics(self, results: goad.Results, batch_size: int):
|
|
133
178
|
"""Update statistics with new batch results.
|
|
134
179
|
|
|
@@ -425,144 +470,84 @@ class Convergence:
|
|
|
425
470
|
converged_status = self._check_convergence()
|
|
426
471
|
return all(converged_status.values())
|
|
427
472
|
|
|
428
|
-
def
|
|
429
|
-
"""
|
|
473
|
+
def _get_convergence_type(self) -> str:
|
|
474
|
+
"""Get the convergence type name for display."""
|
|
475
|
+
class_name = self.__class__.__name__
|
|
476
|
+
if class_name == "EnsembleConvergence":
|
|
477
|
+
return "Ensemble"
|
|
478
|
+
elif class_name == "Convergence":
|
|
479
|
+
return "Standard"
|
|
480
|
+
else:
|
|
481
|
+
return class_name
|
|
482
|
+
|
|
483
|
+
def _get_next_geometry(self, iteration: int) -> Tuple[str, Optional[str]]:
|
|
484
|
+
"""Hook method to get geometry for next batch.
|
|
430
485
|
|
|
431
486
|
Args:
|
|
432
487
|
iteration: Current iteration number
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Tuple of (geom_path, optional_display_info)
|
|
491
|
+
- geom_path: Path to geometry file to use
|
|
492
|
+
- optional_display_info: Optional string to display (e.g., "Geometry: hex.obj")
|
|
433
493
|
"""
|
|
434
|
-
#
|
|
435
|
-
|
|
494
|
+
# Default implementation: use fixed geometry from settings
|
|
495
|
+
return self.settings.geom_path, None
|
|
436
496
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
print(
|
|
440
|
-
f"\nIteration {iteration} ({self.n_orientations}/{min_required} orientations, min not reached):"
|
|
441
|
-
)
|
|
442
|
-
else:
|
|
443
|
-
print(
|
|
444
|
-
f"\nIteration {iteration} ({self.n_orientations} orientations, min {min_required} reached):"
|
|
445
|
-
)
|
|
497
|
+
def _handle_geometry_error(self, error: Exception, geom_path: str) -> bool:
|
|
498
|
+
"""Hook method to handle geometry loading errors.
|
|
446
499
|
|
|
447
|
-
|
|
500
|
+
Args:
|
|
501
|
+
error: The exception that occurred
|
|
502
|
+
geom_path: Path to the geometry that failed
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
True to skip this geometry and continue, False to raise the error
|
|
506
|
+
"""
|
|
507
|
+
# Default implementation: re-raise error (fail fast for single geometry)
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
def _get_theta_bins(self, variable: str) -> Optional[np.ndarray]:
|
|
511
|
+
"""Get theta bins for Mueller elements from batch data."""
|
|
512
|
+
for batch in self.batch_data:
|
|
513
|
+
if "mueller_theta_bins" in batch:
|
|
514
|
+
return batch["mueller_theta_bins"]
|
|
448
515
|
|
|
516
|
+
# Fallback: infer from array length
|
|
517
|
+
mean_array, _ = self._calculate_mean_and_sem_array(variable)
|
|
518
|
+
if len(mean_array) > 0:
|
|
519
|
+
return np.linspace(0, 180, len(mean_array))
|
|
520
|
+
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
def _update_convergence_history(self):
|
|
524
|
+
"""Update convergence history with current SEM values."""
|
|
449
525
|
for conv in self.convergables:
|
|
450
526
|
if conv.is_mueller():
|
|
451
|
-
# Mueller element -
|
|
527
|
+
# Mueller element - track worst SEM
|
|
452
528
|
mean_array, sem_array = self._calculate_mean_and_sem_array(
|
|
453
529
|
conv.variable
|
|
454
530
|
)
|
|
455
|
-
|
|
456
|
-
if len(mean_array) == 0:
|
|
457
|
-
print(f" {conv.variable:<10}: No data yet")
|
|
458
|
-
continue
|
|
459
|
-
|
|
460
|
-
# Get theta bins from results (assuming we have access to bins_1d)
|
|
461
|
-
if hasattr(self, "settings") and hasattr(self.settings, "binning"):
|
|
462
|
-
# We'll get theta values from the first batch's mueller_1d if available
|
|
463
|
-
theta_bins = None
|
|
464
|
-
for batch in self.batch_data:
|
|
465
|
-
if "mueller_theta_bins" in batch:
|
|
466
|
-
theta_bins = batch["mueller_theta_bins"]
|
|
467
|
-
break
|
|
468
|
-
if theta_bins is None:
|
|
469
|
-
theta_bins = np.arange(len(mean_array))
|
|
470
|
-
else:
|
|
471
|
-
theta_bins = np.arange(len(mean_array))
|
|
472
|
-
|
|
473
|
-
# Calculate relative SEM for each theta
|
|
474
|
-
if conv.tolerance_type == "relative":
|
|
475
|
-
relative_sem_array = np.where(
|
|
476
|
-
mean_array != 0, sem_array / np.abs(mean_array), float("inf")
|
|
477
|
-
)
|
|
478
|
-
worst_idx = np.argmax(relative_sem_array)
|
|
479
|
-
worst_sem = relative_sem_array[worst_idx]
|
|
480
|
-
target_str = f"{conv.tolerance * 100:.1f}%"
|
|
481
|
-
current_str = f"{worst_sem * 100:.2f}%"
|
|
482
|
-
else:
|
|
483
|
-
worst_idx = np.argmax(sem_array)
|
|
484
|
-
worst_sem = sem_array[worst_idx]
|
|
485
|
-
target_str = f"{conv.tolerance}"
|
|
486
|
-
current_str = f"{worst_sem:.4g}"
|
|
487
|
-
|
|
488
|
-
worst_theta = theta_bins[worst_idx]
|
|
489
|
-
worst_mean = mean_array[worst_idx]
|
|
490
|
-
|
|
491
|
-
# Count converged bins (either all or specified indices)
|
|
492
|
-
if conv.theta_indices is not None:
|
|
493
|
-
# Only checking specific bins
|
|
494
|
-
indices = [i for i in conv.theta_indices if i < len(mean_array)]
|
|
531
|
+
if len(mean_array) > 0:
|
|
495
532
|
if conv.tolerance_type == "relative":
|
|
496
|
-
|
|
497
|
-
|
|
533
|
+
relative_sem_array = np.where(
|
|
534
|
+
mean_array != 0,
|
|
535
|
+
sem_array / np.abs(mean_array),
|
|
536
|
+
float("inf"),
|
|
498
537
|
)
|
|
538
|
+
worst_sem = np.max(relative_sem_array)
|
|
499
539
|
else:
|
|
500
|
-
|
|
501
|
-
total_bins = len(indices)
|
|
502
|
-
bin_desc = (
|
|
503
|
-
f"θ={[theta_bins[i] for i in indices]}"
|
|
504
|
-
if len(indices) <= 3
|
|
505
|
-
else f"{len(indices)} bins"
|
|
506
|
-
)
|
|
507
|
-
else:
|
|
508
|
-
# Checking all bins
|
|
509
|
-
if conv.tolerance_type == "relative":
|
|
510
|
-
converged_bins = np.sum(relative_sem_array < conv.tolerance)
|
|
511
|
-
else:
|
|
512
|
-
converged_bins = np.sum(sem_array < conv.tolerance)
|
|
513
|
-
total_bins = len(mean_array)
|
|
514
|
-
bin_desc = f"{total_bins} bins"
|
|
515
|
-
|
|
516
|
-
status = "✓" if converged_status[conv.variable] else "❌"
|
|
517
|
-
|
|
518
|
-
# Print Mueller convergence info
|
|
519
|
-
if conv.theta_indices is not None and len(conv.theta_indices) <= 3:
|
|
520
|
-
# For small number of specific bins, show them explicitly
|
|
521
|
-
print(
|
|
522
|
-
f" {conv.variable:<10}: {converged_bins}/{total_bins} {bin_desc} | "
|
|
523
|
-
f"Worst θ={worst_theta:.1f}°: {worst_mean:.4g} | SEM: {current_str} (target: {target_str}) {status}"
|
|
524
|
-
)
|
|
525
|
-
else:
|
|
526
|
-
# For many bins, use standard format
|
|
527
|
-
print(
|
|
528
|
-
f" {conv.variable:<10}: {converged_bins}/{total_bins} bins converged | "
|
|
529
|
-
f"Worst θ={worst_theta:.1f}°: {worst_mean:.4g} | SEM: {current_str} (target: {target_str}) {status}"
|
|
530
|
-
)
|
|
540
|
+
worst_sem = np.max(sem_array)
|
|
531
541
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
)
|
|
542
|
+
self.convergence_history.append(
|
|
543
|
+
(self.n_orientations, conv.variable, worst_sem)
|
|
544
|
+
)
|
|
536
545
|
else:
|
|
537
546
|
# Scalar variable
|
|
538
547
|
mean, sem = self._calculate_mean_and_sem(conv.variable)
|
|
548
|
+
if conv.tolerance_type == "relative" and mean != 0:
|
|
549
|
+
sem = sem / abs(mean)
|
|
539
550
|
|
|
540
|
-
# Calculate 95% CI
|
|
541
|
-
ci_lower = mean - 1.96 * sem
|
|
542
|
-
ci_upper = mean + 1.96 * sem
|
|
543
|
-
|
|
544
|
-
# Format based on tolerance type
|
|
545
|
-
if conv.tolerance_type == "relative":
|
|
546
|
-
if mean != 0:
|
|
547
|
-
relative_sem = sem / abs(mean)
|
|
548
|
-
target_str = f"{conv.tolerance * 100:.1f}%"
|
|
549
|
-
current_str = f"{relative_sem * 100:.2f}%"
|
|
550
|
-
else:
|
|
551
|
-
target_str = f"{conv.tolerance} (abs, mean=0)"
|
|
552
|
-
current_str = f"{sem:.4g}"
|
|
553
|
-
else:
|
|
554
|
-
target_str = f"{conv.tolerance}"
|
|
555
|
-
current_str = f"{sem:.4g}"
|
|
556
|
-
|
|
557
|
-
# Status indicator
|
|
558
|
-
status = "✓" if converged_status[conv.variable] else "❌"
|
|
559
|
-
|
|
560
|
-
# Print line with mean, SEM, CI, and convergence status
|
|
561
|
-
print(
|
|
562
|
-
f" {conv.variable:<10}: {mean:.6f} ± {sem:.6f} [{ci_lower:.6f}, {ci_upper:.6f}] | SEM: {current_str} (target: {target_str}) {status}"
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
# Add to convergence history
|
|
566
551
|
self.convergence_history.append(
|
|
567
552
|
(self.n_orientations, conv.variable, sem)
|
|
568
553
|
)
|
|
@@ -577,30 +562,99 @@ class Convergence:
|
|
|
577
562
|
converged = False
|
|
578
563
|
warning = None
|
|
579
564
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
565
|
+
# Create Live context for smooth updating display
|
|
566
|
+
with self._display.create_live_context() as live:
|
|
567
|
+
# Show initial display before first batch
|
|
568
|
+
initial_display = self._display.build_display(
|
|
569
|
+
iteration=0,
|
|
570
|
+
n_orientations=self.n_orientations,
|
|
571
|
+
get_stats=self._calculate_mean_and_sem,
|
|
572
|
+
get_array_stats=self._calculate_mean_and_sem_array,
|
|
573
|
+
get_bin_labels=self._get_theta_bins,
|
|
574
|
+
power_ratio=None,
|
|
575
|
+
geom_info=None,
|
|
576
|
+
)
|
|
577
|
+
live.update(initial_display)
|
|
578
|
+
|
|
579
|
+
while not converged and self.n_orientations < self.max_orientations:
|
|
580
|
+
iteration += 1
|
|
581
|
+
|
|
582
|
+
# Get geometry for this batch (hook method - can be overridden)
|
|
583
|
+
geom_path, geom_info = self._get_next_geometry(iteration)
|
|
584
|
+
|
|
585
|
+
# Determine batch size for this iteration
|
|
586
|
+
remaining = self.max_orientations - self.n_orientations
|
|
587
|
+
batch_size = min(self.batch_size, remaining)
|
|
588
|
+
|
|
589
|
+
# Set batch size
|
|
590
|
+
orientations = goad.create_uniform_orientation(batch_size)
|
|
591
|
+
|
|
592
|
+
# Set the geometry path and orientations for the settings
|
|
593
|
+
self.settings.geom_path = geom_path
|
|
594
|
+
self.settings.orientation = orientations
|
|
595
|
+
|
|
596
|
+
# Run MultiProblem with error handling for bad geometries
|
|
597
|
+
# Suppress Rust progress bars by redirecting stderr at fd level
|
|
598
|
+
try:
|
|
599
|
+
mp = goad.MultiProblem(self.settings)
|
|
600
|
+
# Redirect stderr file descriptor to suppress Rust progress bars
|
|
601
|
+
stderr_fd = sys.stderr.fileno()
|
|
602
|
+
with open(os.devnull, "w") as devnull:
|
|
603
|
+
old_stderr_fd = os.dup(stderr_fd)
|
|
604
|
+
try:
|
|
605
|
+
os.dup2(devnull.fileno(), stderr_fd)
|
|
606
|
+
mp.py_solve()
|
|
607
|
+
finally:
|
|
608
|
+
os.dup2(old_stderr_fd, stderr_fd)
|
|
609
|
+
os.close(old_stderr_fd)
|
|
610
|
+
except Exception as e:
|
|
611
|
+
# Geometry loading failed (bad faces, degenerate geometry, etc.)
|
|
612
|
+
# Check if subclass wants to handle this error (e.g., skip for ensemble)
|
|
613
|
+
if self._handle_geometry_error(e, geom_path):
|
|
614
|
+
# Skip this geometry and continue
|
|
615
|
+
continue
|
|
616
|
+
else:
|
|
617
|
+
# For single-geometry convergence, we can't skip - must raise error
|
|
618
|
+
error_msg = (
|
|
619
|
+
f"Failed to initialize MultiProblem with geometry '{geom_path}': {e}\n"
|
|
620
|
+
f"Please check geometry file for:\n"
|
|
621
|
+
f" - Degenerate faces (area = 0)\n"
|
|
622
|
+
f" - Non-planar geometry\n"
|
|
623
|
+
f" - Faces that are too small\n"
|
|
624
|
+
f" - Invalid mesh topology\n"
|
|
625
|
+
f" - Geometry file corruption"
|
|
626
|
+
)
|
|
627
|
+
raise type(e)(error_msg) from e
|
|
628
|
+
|
|
629
|
+
# Update statistics
|
|
630
|
+
self._update_statistics(mp.results, batch_size)
|
|
631
|
+
|
|
632
|
+
# Extract power ratio from results
|
|
633
|
+
try:
|
|
634
|
+
powers = mp.results.powers # It's a property, not a method
|
|
635
|
+
power_in = powers.get("input", 1.0)
|
|
636
|
+
power_out = powers.get("output", 0.0)
|
|
637
|
+
power_ratio = power_out / power_in if power_in > 0 else 0.0
|
|
638
|
+
except Exception:
|
|
639
|
+
power_ratio = None
|
|
640
|
+
|
|
641
|
+
# Update convergence history
|
|
642
|
+
self._update_convergence_history()
|
|
643
|
+
|
|
644
|
+
# Update live display with optional geometry info
|
|
645
|
+
display = self._display.build_display(
|
|
646
|
+
iteration=iteration,
|
|
647
|
+
n_orientations=self.n_orientations,
|
|
648
|
+
get_stats=self._calculate_mean_and_sem,
|
|
649
|
+
get_array_stats=self._calculate_mean_and_sem_array,
|
|
650
|
+
get_bin_labels=self._get_theta_bins,
|
|
651
|
+
power_ratio=power_ratio,
|
|
652
|
+
geom_info=geom_info,
|
|
653
|
+
)
|
|
654
|
+
live.update(display)
|
|
601
655
|
|
|
602
|
-
|
|
603
|
-
|
|
656
|
+
# Check convergence
|
|
657
|
+
converged = self._all_converged()
|
|
604
658
|
|
|
605
659
|
# Prepare final results
|
|
606
660
|
if converged:
|
|
@@ -728,155 +782,49 @@ class EnsembleConvergence(Convergence):
|
|
|
728
782
|
mueller_2d=mueller_2d,
|
|
729
783
|
)
|
|
730
784
|
|
|
731
|
-
|
|
732
|
-
|
|
785
|
+
# Track skipped geometries for error handling
|
|
786
|
+
self.skipped_geometries = []
|
|
733
787
|
|
|
734
|
-
|
|
735
|
-
|
|
788
|
+
def _get_next_geometry(self, iteration: int) -> Tuple[str, Optional[str]]:
|
|
789
|
+
"""Override to randomly select geometry from ensemble.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
iteration: Current iteration number
|
|
736
793
|
|
|
737
794
|
Returns:
|
|
738
|
-
|
|
795
|
+
Tuple of (geom_path, display_info)
|
|
739
796
|
"""
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
while not converged and self.n_orientations < self.max_orientations:
|
|
745
|
-
iteration += 1
|
|
746
|
-
|
|
747
|
-
# Randomly select a geometry file for this batch
|
|
748
|
-
geom_file = random.choice(self.geom_files)
|
|
749
|
-
geom_path = os.path.join(self.geom_dir, geom_file)
|
|
750
|
-
|
|
751
|
-
# Determine batch size for this iteration
|
|
752
|
-
remaining = self.max_orientations - self.n_orientations
|
|
753
|
-
batch_size = min(self.batch_size, remaining)
|
|
754
|
-
|
|
755
|
-
# Create orientations for this batch
|
|
756
|
-
orientations = goad.create_uniform_orientation(batch_size)
|
|
757
|
-
|
|
758
|
-
# Update settings with selected geometry and orientations
|
|
759
|
-
self.settings.geom_path = geom_path
|
|
760
|
-
self.settings.orientation = orientations
|
|
761
|
-
|
|
762
|
-
# Run MultiProblem with selected geometry
|
|
763
|
-
mp = goad.MultiProblem(self.settings)
|
|
764
|
-
mp.py_solve()
|
|
765
|
-
|
|
766
|
-
# Update statistics
|
|
767
|
-
self._update_statistics(mp.results, batch_size)
|
|
768
|
-
|
|
769
|
-
# Print progress (with geometry info)
|
|
770
|
-
min_required = self.min_batches * self.batch_size
|
|
771
|
-
if self.n_orientations < min_required:
|
|
772
|
-
print(
|
|
773
|
-
f"\nIteration {iteration} ({self.n_orientations}/{min_required} orientations, min not reached) - Geometry: {geom_file}"
|
|
774
|
-
)
|
|
775
|
-
else:
|
|
776
|
-
print(
|
|
777
|
-
f"\nIteration {iteration} ({self.n_orientations} orientations, min {min_required} reached) - Geometry: {geom_file}"
|
|
778
|
-
)
|
|
779
|
-
self._print_progress_without_header(iteration)
|
|
780
|
-
|
|
781
|
-
# Check convergence
|
|
782
|
-
converged = self._all_converged()
|
|
783
|
-
|
|
784
|
-
# Prepare final results
|
|
785
|
-
if converged:
|
|
786
|
-
print(f"\nConverged after {self.n_orientations} orientations.")
|
|
787
|
-
else:
|
|
788
|
-
warning = f"Maximum orientations ({self.max_orientations}) reached without convergence"
|
|
789
|
-
print(f"\nWarning: {warning}")
|
|
790
|
-
|
|
791
|
-
# Calculate final values and SEMs
|
|
792
|
-
final_values = {}
|
|
793
|
-
final_sems = {}
|
|
794
|
-
for conv in self.convergables:
|
|
795
|
-
if conv.is_mueller():
|
|
796
|
-
mean_array, sem_array = self._calculate_mean_and_sem_array(
|
|
797
|
-
conv.variable
|
|
798
|
-
)
|
|
799
|
-
final_values[conv.variable] = mean_array
|
|
800
|
-
final_sems[conv.variable] = sem_array
|
|
801
|
-
else:
|
|
802
|
-
mean, sem = self._calculate_mean_and_sem(conv.variable)
|
|
803
|
-
final_values[conv.variable] = mean
|
|
804
|
-
final_sems[conv.variable] = sem
|
|
805
|
-
|
|
806
|
-
# Prepare Mueller matrices with SEM
|
|
807
|
-
mueller_1d = None
|
|
808
|
-
mueller_1d_sem = None
|
|
809
|
-
mueller_2d = None
|
|
810
|
-
|
|
811
|
-
if self.mueller_1d and self.mueller_1d_sum is not None:
|
|
812
|
-
mueller_1d = self.mueller_1d_sum / self.n_orientations
|
|
813
|
-
|
|
814
|
-
# Compute SEM for all 16 Mueller elements
|
|
815
|
-
# mueller_1d shape: (n_theta, 16)
|
|
816
|
-
n_theta = mueller_1d.shape[0]
|
|
817
|
-
mueller_1d_sem = np.zeros_like(mueller_1d)
|
|
818
|
-
|
|
819
|
-
for row in range(1, 5):
|
|
820
|
-
for col in range(1, 5):
|
|
821
|
-
element_name = f"S{row}{col}"
|
|
822
|
-
mueller_idx = (row - 1) * 4 + (col - 1)
|
|
823
|
-
|
|
824
|
-
# Calculate mean and SEM for this element across all theta bins
|
|
825
|
-
mean_array, sem_array = self._calculate_mean_and_sem_array(
|
|
826
|
-
element_name
|
|
827
|
-
)
|
|
828
|
-
|
|
829
|
-
if len(sem_array) > 0:
|
|
830
|
-
mueller_1d_sem[:, mueller_idx] = sem_array
|
|
831
|
-
|
|
832
|
-
# Store mueller_1d_sem in final_values for unified API access
|
|
833
|
-
final_values["mueller_1d_sem"] = mueller_1d_sem
|
|
797
|
+
# Randomly select a geometry file for this batch
|
|
798
|
+
geom_file = random.choice(self.geom_files)
|
|
799
|
+
geom_path = os.path.join(self.geom_dir, geom_file)
|
|
800
|
+
geom_info = f"Geom: {geom_file}"
|
|
834
801
|
|
|
835
|
-
|
|
836
|
-
mueller_2d = self.mueller_2d_sum / self.n_orientations
|
|
802
|
+
return geom_path, geom_info
|
|
837
803
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
n_orientations=self.n_orientations,
|
|
841
|
-
values=final_values,
|
|
842
|
-
sem_values=final_sems,
|
|
843
|
-
mueller_1d=mueller_1d,
|
|
844
|
-
mueller_2d=mueller_2d,
|
|
845
|
-
convergence_history=self.convergence_history,
|
|
846
|
-
warning=warning,
|
|
847
|
-
)
|
|
804
|
+
def _handle_geometry_error(self, error: Exception, geom_path: str) -> bool:
|
|
805
|
+
"""Override to skip bad geometries in ensemble mode.
|
|
848
806
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
807
|
+
Args:
|
|
808
|
+
error: The exception that occurred
|
|
809
|
+
geom_path: Path to the geometry that failed
|
|
852
810
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
ci_upper = mean + 1.96 * sem
|
|
859
|
-
|
|
860
|
-
# Format based on tolerance type
|
|
861
|
-
if conv.tolerance_type == "relative":
|
|
862
|
-
if mean != 0:
|
|
863
|
-
relative_sem = sem / abs(mean)
|
|
864
|
-
target_str = f"{conv.tolerance * 100:.1f}%"
|
|
865
|
-
current_str = f"{relative_sem * 100:.2f}%"
|
|
866
|
-
else:
|
|
867
|
-
target_str = f"{conv.tolerance} (abs, mean=0)"
|
|
868
|
-
current_str = f"{sem:.4g}"
|
|
869
|
-
else:
|
|
870
|
-
target_str = f"{conv.tolerance}"
|
|
871
|
-
current_str = f"{sem:.4g}"
|
|
811
|
+
Returns:
|
|
812
|
+
True to skip this geometry and continue
|
|
813
|
+
"""
|
|
814
|
+
# Extract just the filename
|
|
815
|
+
geom_file = os.path.basename(geom_path)
|
|
872
816
|
|
|
873
|
-
|
|
874
|
-
|
|
817
|
+
# Print warning and track skipped geometry
|
|
818
|
+
print(f"\nWarning: Skipping geometry '{geom_file}': {error}")
|
|
819
|
+
self.skipped_geometries.append(geom_file)
|
|
875
820
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
821
|
+
# Check if all geometries have been skipped
|
|
822
|
+
if len(self.skipped_geometries) >= len(self.geom_files):
|
|
823
|
+
raise ValueError(
|
|
824
|
+
f"All {len(self.geom_files)} geometry files failed to load. "
|
|
825
|
+
"Please check geometry files for degenerate faces, non-planar geometry, "
|
|
826
|
+
"or faces that are too small."
|
|
879
827
|
)
|
|
880
828
|
|
|
881
|
-
|
|
882
|
-
|
|
829
|
+
# Skip this geometry and continue
|
|
830
|
+
return True
|