Skip to content

Commit 584fbae

Browse files
committed
added multi-start for stokes calculation to avoid incorrect minima
1 parent e234cba commit 584fbae

8 files changed

Lines changed: 1071 additions & 96 deletions

File tree

examples/debug_optimization.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
"""
2+
Example script demonstrating the optimization debugging visualization.
3+
4+
This script shows how to use the new debugging features to visualize
5+
how the optimizer converges to the stress tensor solution and how the
6+
predicted Stokes parameters evolve during optimization.
7+
"""
8+
9+
import matplotlib.pyplot as plt
10+
import numpy as np
11+
12+
from photoelastimetry.plotting import plot_optimization_history
13+
from photoelastimetry.solver.stokes_solver import (
14+
compute_normalized_stokes,
15+
compute_stokes_components,
16+
predict_stokes,
17+
recover_stress_tensor,
18+
recover_stress_tensor_live,
19+
)
20+
21+
22+
def create_synthetic_data():
23+
"""Create synthetic measurement data for testing."""
24+
# True stress state (unknown to optimizer)
25+
# Use larger stresses to create measurable photoelastic effect
26+
sigma_xx_true = 500000.0 # Pa (500 kPa)
27+
sigma_yy_true = 200000.0 # Pa (200 kPa)
28+
sigma_xy_true = 100000.0 # Pa (100 kPa)
29+
30+
# Material and optical properties
31+
wavelengths = np.array([650e-9, 532e-9, 473e-9]) # R, G, B wavelengths (m)
32+
C_values = np.array([2.3e-10, 2.5e-10, 2.7e-10]) # Stress-optic coefficients (1/Pa)
33+
nu = 1.0 # Solid fraction
34+
L = 0.01 # Sample thickness (m)
35+
S_i_hat = np.array([1.0, 0.0]) # Incoming linearly polarized light (horizontal)
36+
37+
# Generate "measured" Stokes components by forward prediction
38+
S_m_hat = np.zeros((3, 2))
39+
for c in range(3):
40+
S_m_hat[c] = predict_stokes(
41+
sigma_xx_true, sigma_yy_true, sigma_xy_true, C_values[c], nu, L, wavelengths[c], S_i_hat
42+
)
43+
44+
# Add small noise to make it realistic
45+
np.random.seed(42)
46+
S_m_hat += np.random.normal(0, 0.001, S_m_hat.shape)
47+
48+
return S_m_hat, wavelengths, C_values, nu, L, S_i_hat, (sigma_xx_true, sigma_yy_true, sigma_xy_true)
49+
50+
51+
def example_1_static_plot():
52+
"""Example 1: Generate a static plot after optimization completes."""
53+
print("\n" + "=" * 60)
54+
print("Example 1: Static optimization history plot")
55+
print("=" * 60)
56+
57+
# Create synthetic data
58+
S_m_hat, wavelengths, C_values, nu, L, S_i_hat, true_stress = create_synthetic_data()
59+
60+
print("\nTrue stress state:")
61+
print(f" σ_xx = {true_stress[0]:.2f} Pa")
62+
print(f" σ_yy = {true_stress[1]:.2f} Pa")
63+
print(f" σ_xy = {true_stress[2]:.2f} Pa")
64+
65+
# Run optimization with history tracking
66+
print("\nRunning optimization with history tracking...")
67+
initial_guess = np.array([100000.0, 100000.0, 10000.0]) # Deliberately poor initial guess
68+
69+
stress_recovered, success, history = recover_stress_tensor(
70+
S_m_hat, wavelengths, C_values, nu, L, S_i_hat, initial_guess=initial_guess, track_history=True
71+
)
72+
73+
print(f"\nOptimization {'succeeded' if success else 'failed'}")
74+
best_path = history["all_paths"][history["best_path_index"]]
75+
print(f"Number of paths explored: {len(history['all_paths'])}")
76+
print(f"Best path iterations: {len(best_path['residuals'])}")
77+
print(f"\nRecovered stress state:")
78+
print(f" σ_xx = {stress_recovered[0]:.2f} Pa")
79+
print(f" σ_yy = {stress_recovered[1]:.2f} Pa")
80+
print(f" σ_xy = {stress_recovered[2]:.2f} Pa")
81+
print(f"\nFinal residual: {best_path['residuals'][-1]:.2e}")
82+
83+
# Check if we found an equivalent solution (photoelastic ambiguity)
84+
# The photoelastic effect has ambiguity: swapping σ_xx <-> σ_yy and flipping σ_xy gives same result
85+
if abs(stress_recovered[0] - true_stress[1]) < abs(stress_recovered[0] - true_stress[0]) * 0.1:
86+
print("\nNote: Found equivalent solution due to photoelastic ambiguity")
87+
print(f" (σ_xx, σ_yy, σ_xy) ≈ (σ_yy_true, σ_xx_true, -σ_xy_true)")
88+
print(f" This gives identical Stokes parameters!")
89+
90+
# Create detailed static plot
91+
print("\nGenerating optimization history plot...")
92+
fig = plot_optimization_history(history, S_m_hat, filename="optimization_history.png")
93+
print("Saved plot to: optimization_history.png")
94+
95+
return history, S_m_hat
96+
97+
98+
def example_2_live_plot():
99+
"""Example 2: Watch optimization progress in real-time."""
100+
print("\n" + "=" * 60)
101+
print("Example 2: Live optimization visualization")
102+
print("=" * 60)
103+
print("\nThis will open a live-updating plot window.")
104+
print("Watch how the optimizer searches the stress space!")
105+
106+
# Create synthetic data
107+
S_m_hat, wavelengths, C_values, nu, L, S_i_hat, true_stress = create_synthetic_data()
108+
109+
print("\nTrue stress state:")
110+
print(f" σ_xx = {true_stress[0]:.2f} Pa")
111+
print(f" σ_yy = {true_stress[1]:.2f} Pa")
112+
print(f" σ_xy = {true_stress[2]:.2f} Pa")
113+
114+
# Run with live plotting (updates every 5 iterations)
115+
print("\nStarting live optimization...")
116+
initial_guess = np.array([100000.0, 100000.0, 10000.0])
117+
118+
stress_recovered, success, history, fig = recover_stress_tensor_live(
119+
S_m_hat,
120+
wavelengths,
121+
C_values,
122+
nu,
123+
L,
124+
S_i_hat,
125+
initial_guess=initial_guess,
126+
update_interval=5, # Update plot every 5 iterations
127+
)
128+
129+
print(f"\nOptimization completed!")
130+
print(
131+
f"Recovered stress: σ_xx={stress_recovered[0]:.2f}, "
132+
f"σ_yy={stress_recovered[1]:.2f}, σ_xy={stress_recovered[2]:.2f} Pa"
133+
)
134+
print(f"Final residual: {history['residuals'][-1]:.2e}")
135+
136+
# Keep window open
137+
print("\nClose the plot window to continue...")
138+
plt.show()
139+
140+
return stress_recovered, history
141+
142+
143+
def example_3_compare_initial_guesses():
144+
"""Example 3: Compare optimization paths from different initial guesses."""
145+
print("\n" + "=" * 60)
146+
print("Example 3: Compare different initial guesses")
147+
print("=" * 60)
148+
149+
# Create synthetic data
150+
S_m_hat, wavelengths, C_values, nu, L, S_i_hat, true_stress = create_synthetic_data()
151+
152+
initial_guesses = [
153+
np.array([100000.0, 100000.0, 10000.0]),
154+
np.array([800000.0, 800000.0, 200000.0]),
155+
np.array([300000.0, 600000.0, -50000.0]),
156+
]
157+
158+
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
159+
fig.suptitle("Comparison of Different Initial Guesses", fontsize=14, fontweight="bold")
160+
161+
colors = ["blue", "red", "green"]
162+
163+
for i, (guess, color) in enumerate(zip(initial_guesses, colors)):
164+
print(f"\nTesting initial guess {i+1}: [{guess[0]:.0f}, {guess[1]:.0f}, {guess[2]:.0f}]")
165+
166+
stress, success, history = recover_stress_tensor(
167+
S_m_hat, wavelengths, C_values, nu, L, S_i_hat, initial_guess=guess, track_history=True
168+
)
169+
170+
best_path = history["all_paths"][history["best_path_index"]]
171+
stress_params = best_path["stress_params"]
172+
residuals = best_path["residuals"]
173+
iterations = np.arange(len(residuals))
174+
175+
# Plot stress component evolution
176+
axes[0, 0].plot(iterations, stress_params[:, 0], color=color, alpha=0.7, label=f"Guess {i+1}")
177+
axes[0, 0].set_ylabel("σ_xx (Pa)")
178+
axes[0, 0].set_title("σ_xx Evolution")
179+
axes[0, 0].legend()
180+
axes[0, 0].grid(True, alpha=0.3)
181+
182+
axes[0, 1].plot(iterations, stress_params[:, 1], color=color, alpha=0.7, label=f"Guess {i+1}")
183+
axes[0, 1].set_ylabel("σ_yy (Pa)")
184+
axes[0, 1].set_title("σ_yy Evolution")
185+
axes[0, 1].legend()
186+
axes[0, 1].grid(True, alpha=0.3)
187+
188+
axes[1, 0].plot(iterations, stress_params[:, 2], color=color, alpha=0.7, label=f"Guess {i+1}")
189+
axes[1, 0].set_xlabel("Iteration")
190+
axes[1, 0].set_ylabel("σ_xy (Pa)")
191+
axes[1, 0].set_title("σ_xy Evolution")
192+
axes[1, 0].legend()
193+
axes[1, 0].grid(True, alpha=0.3)
194+
195+
# Plot residual evolution
196+
axes[1, 1].semilogy(iterations, residuals, color=color, alpha=0.7, label=f"Guess {i+1}")
197+
axes[1, 1].set_xlabel("Iteration")
198+
axes[1, 1].set_ylabel("Residual (log scale)")
199+
axes[1, 1].set_title("Residual Evolution")
200+
axes[1, 1].legend()
201+
axes[1, 1].grid(True, alpha=0.3)
202+
203+
print(f" Final: [{stress[0]:.2f}, {stress[1]:.2f}, {stress[2]:.2f}] Pa")
204+
print(f" Iterations: {len(residuals)}, Final residual: {residuals[-1]:.2e}")
205+
206+
# Add true values as horizontal lines
207+
for ax, idx, name in [(axes[0, 0], 0, "σ_xx"), (axes[0, 1], 1, "σ_yy"), (axes[1, 0], 2, "σ_xy")]:
208+
ax.axhline(
209+
true_stress[idx], color="black", linestyle="--", linewidth=2, alpha=0.5, label="True value"
210+
)
211+
ax.legend()
212+
213+
plt.tight_layout()
214+
plt.savefig("initial_guess_comparison.png", dpi=150)
215+
print("\nSaved comparison plot to: initial_guess_comparison.png")
216+
plt.show()
217+
218+
219+
def main():
220+
"""Run all examples."""
221+
print("\n" + "=" * 60)
222+
print("Optimization Debugging Examples")
223+
print("=" * 60)
224+
print("\nThese examples demonstrate how to visualize the stress")
225+
print("tensor optimization process and debug convergence issues.")
226+
227+
# Example 1: Static plot after optimization
228+
example_1_static_plot()
229+
230+
# Example 2: Live updating plot (interactive) - TODO: update for new history format
231+
# response = input("\nRun live plotting example? (y/n): ").strip().lower()
232+
# if response == "y":
233+
# example_2_live_plot()
234+
235+
# Example 3: Compare initial guesses
236+
response = input("\nCompare different initial guesses? (y/n): ").strip().lower()
237+
if response == "y":
238+
example_3_compare_initial_guesses()
239+
240+
print("\n" + "=" * 60)
241+
print("Examples completed!")
242+
print("=" * 60)
243+
244+
245+
if __name__ == "__main__":
246+
main()

json/disk.json5

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
// Incoming normalized Stokes vector [S1_hat, S2_hat, S3_hat]
3+
S_i_hat : [1.0, 0.0, 0.0], // Incoming polarization state
4+
binning: 8,
5+
crop : [340, 634, 276, 565], // Crop region as [x1, x2, y1, y2]
6+
wavelengths : [650, 550, 450], // R, G, B wavelengths (nm)
7+
C : [3e-9,3e-9,3e-9], // stress-optic coefficient for each wavelength (1/Pa)
8+
thickness : 0.01, // thickness of sample in m
9+
polarisation_efficiency : 0.95,
10+
folderName : "/Volumes/PRJ-PSM/eCaptureProData/2025_04_15/2025_04_15_15_40_24_898/02002060_diskSaving1/",
11+
output_filename : "images/disk/predicted_stress_map.tiff",
12+
debug: false,
13+
solver: "stokes"
14+
}

photoelastimetry/io.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ def save_image(filename, data, metadata={}):
148148
>>> metadata = {"dtype": "uint8"}
149149
>>> save_image("output.png", data, metadata)
150150
"""
151+
output_folder = os.path.dirname(filename)
152+
if output_folder != "" and not os.path.exists(output_folder):
153+
os.makedirs(output_folder)
154+
151155
if filename.endswith(".npy"):
152156
np.save(filename, data)
153157
elif filename.endswith(".raw"):

photoelastimetry/main.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import photoelastimetry.io
99
import photoelastimetry.plotting
10+
import photoelastimetry.solver.equilibrium_solver
11+
import photoelastimetry.solver.intensity_solver
1012
import photoelastimetry.solver.stokes_solver
1113

1214

@@ -49,11 +51,13 @@ def image_to_stress(params, output_filename=None):
4951

5052
if params.get("crop") is not None:
5153
data = data[
52-
params["crop"][0] : params["crop"][1],
5354
params["crop"][2] : params["crop"][3],
55+
params["crop"][0] : params["crop"][1],
5456
:,
5557
:,
5658
]
59+
if params["debug"]:
60+
photoelastimetry.io.save_image("debug_cropped_image.tiff", data, metadata)
5761

5862
if params["debug"]:
5963
import matplotlib.pyplot as plt
@@ -72,9 +76,8 @@ def image_to_stress(params, output_filename=None):
7276

7377
C = params["C"] # Stress-optic coefficients in 1/Pa
7478
L = params["thickness"] # Thickness in m
75-
wavelengths_nm = np.array(params["wavelengths"]) # Wavelengths in nm
79+
WAVELENGTHS = np.array(params["wavelengths"]) * 1e-9 # Wavelengths in m
7680
NU = 1.0 # Solid sample
77-
WAVELENGTHS = wavelengths_nm * 1e-9 # Convert to meters
7881
if isinstance(C, list) or isinstance(C, np.ndarray):
7982
C_VALUES = C
8083
else:
@@ -92,15 +95,38 @@ def image_to_stress(params, output_filename=None):
9295

9396
# Calculate stress map from image
9497
n_jobs = params.get("n_jobs", -1) # Default to using all cores
95-
stress_map = photoelastimetry.solver.stokes_solver.recover_stress_map_stokes(
96-
data,
97-
WAVELENGTHS,
98-
C_VALUES,
99-
NU,
100-
L,
101-
S_I_HAT,
102-
n_jobs=n_jobs,
103-
)
98+
if params.get("solver") == "stokes":
99+
stress_map = photoelastimetry.solver.stokes_solver.recover_stress_map_stokes(
100+
data,
101+
WAVELENGTHS,
102+
C_VALUES,
103+
NU,
104+
L,
105+
S_I_HAT,
106+
n_jobs=n_jobs,
107+
)
108+
elif params.get("solver") == "intensity":
109+
stress_map, success_map = photoelastimetry.solver.intensity_solver.recover_stress_map_intensity(
110+
data,
111+
WAVELENGTHS,
112+
C_VALUES,
113+
NU,
114+
L,
115+
S_I_HAT,
116+
n_jobs=n_jobs,
117+
)
118+
elif params.get("solver") == "equilibrium":
119+
stress_map = photoelastimetry.solver.equilibrium_solver.recover_stress_field_global_iterative(
120+
data,
121+
WAVELENGTHS,
122+
C_VALUES,
123+
NU,
124+
L,
125+
S_I_HAT,
126+
n_jobs=n_jobs,
127+
)
128+
else:
129+
raise ValueError("Solver type not recognized. Use 'stokes', 'intensity', or 'equilibrium'.")
104130

105131
if params.get("output_filename") is not None:
106132
output_filename = params["output_filename"]

0 commit comments

Comments
 (0)