Add WFE PSD analysis tools (Tony Hull methodology)
- tools/wfe_psd.py: Quick PSD computation for WFE surfaces - optimization_engine/insights/wfe_psd.py: Full PSD module with band decomposition (LSF/MSF/HSF), radial averaging, Hann windowing, and visualization support - knowledge_base/lac/session_insights/stopp_command_20260129.jsonl: Session insight from stopp command implementation PSD analysis decomposes WFE into spatial frequency bands per Tony Hull's JWST methodology. Used for CDR V7 to validate that MSF (support print-through) dominates the residual WFE at 85-89% of total RMS.
This commit is contained in:
144
tools/wfe_psd.py
Normal file
144
tools/wfe_psd.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick PSD implementation for Tony Hull's WFE approach
|
||||
This adds PSD analysis to replace the LSF/MSF metrics
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from scipy.interpolate import griddata
|
||||
from scipy.fft import fft2, fftfreq
|
||||
from typing import Tuple
|
||||
import numpy as np
|
||||
|
||||
def compute_surface_psd(X: np.ndarray, Y: np.ndarray, Z: np.ndarray,
|
||||
aperture_radius: float) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Compute surface PSD using Tony Hull's methodology.
|
||||
|
||||
Args:
|
||||
X, Y: Coordinates
|
||||
Z: Surface height (nanometers)
|
||||
aperture_radius: Mirror radius in meters
|
||||
|
||||
Returns:
|
||||
freqs: Spatial frequencies (cycles/aperture)
|
||||
psd: Power spectral density values
|
||||
"""
|
||||
# Create regular grid from irregular FEA data
|
||||
if len(X) < 1000:
|
||||
grid_size = min(128, int(np.sqrt(len(X))))
|
||||
else:
|
||||
grid_size = 256
|
||||
|
||||
# Build regular grid
|
||||
x_range = np.linspace(X.min(), X.max(), grid_size)
|
||||
y_range = np.linspace(Y.min(), Y.max(), grid_size)
|
||||
X_grid, Y_grid = np.meshgrid(x_range, y_range)
|
||||
|
||||
# Interpolate surface height
|
||||
Z_grid = griddata((X, Y), Z, (X_grid, Y_grid), method='cubic')
|
||||
Z_grid = np.nan_to_num(Z_grid, nan=0.0)
|
||||
|
||||
# Apply Hann window to reduce edge effects
|
||||
hann_window = np.outer(np.hanning(grid_size), np.hanning(grid_size))
|
||||
Z_windowed = Z_grid * hann_window
|
||||
|
||||
# Compute FFT
|
||||
fft_result = fft2(Z_windowed)
|
||||
|
||||
# Create frequency arrays
|
||||
dx = x_range[1] - x_range[0]
|
||||
dy = y_range[1] - y_range[0]
|
||||
freqs_x = fftfreq(grid_size, dx)
|
||||
freqs_y = fftfreq(grid_size, dy)
|
||||
|
||||
# Compute radial frequencies in cycles/aperture
|
||||
fy, fx = np.meshgrid(freqs_y, freqs_x)
|
||||
freqs_radial = np.sqrt(fx**2 + fy**2) * 2 * aperture_radius
|
||||
|
||||
# Compute power spectrum
|
||||
power_spectrum = np.abs(fft_result)**2
|
||||
|
||||
# Create radial bins for averaging
|
||||
bin_edges = np.logspace(-2, 2, 50) # Logarithmic spacing
|
||||
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
||||
|
||||
psd_radial = []
|
||||
for i in range(len(bin_edges)-1):
|
||||
mask = (freqs_radial >= bin_edges[i]) & (freqs_radial < bin_edges[i+1])
|
||||
if np.any(mask):
|
||||
psd_radial.append(np.mean(power_spectrum[mask]) * (dx * dy))
|
||||
else:
|
||||
psd_radial.append(0.0)
|
||||
|
||||
return bin_centers, np.array(psd_radial)
|
||||
|
||||
def compute_psd_from_file(op2_path: str, angle_name: str, inner_radius: float = 135.75) -> Tuple[np.ndarray, np.ndarray, float]:
|
||||
"""
|
||||
Compute PSD from FEA data file.
|
||||
|
||||
Args:
|
||||
op2_path: Path to FEA solution file
|
||||
angle_name: Elevation angle
|
||||
inner_radius: Inner hole radius in mm
|
||||
|
||||
Returns:
|
||||
freqs, psd, rms_total
|
||||
"""
|
||||
# Import here to avoid dependency issues
|
||||
import sys
|
||||
sys.path.insert(0, '/home/papa/repos/Atomizer')
|
||||
from optimization_engine.extractors.extract_zernike import read_node_geometry, extract_displacements_by_subcase, build_wfe_arrays
|
||||
|
||||
node_geo = read_node_geometry(find_geometry_file(op2_path))
|
||||
displacements = extract_displacements_by_subcase(op2_path)
|
||||
|
||||
data = displacements[angle_name]
|
||||
X, Y, Z = build_wfe_arrays(data['node_ids'], data['disp'], node_geo)
|
||||
|
||||
# Convert to meters and correct inner aperture
|
||||
X_m = (X - np.mean(X)) / 1000
|
||||
Y_m = (Y - np.mean(Y)) / 1000
|
||||
Z_nm = Z * 1000 # Convert to nanometers
|
||||
|
||||
# Mask for inner hole
|
||||
r = np.sqrt(X_m**2 + Y_m**2)
|
||||
mask = r * 1000 <= 0.575 # Assuming 575mm total radius
|
||||
|
||||
X_masked = X_m[mask]
|
||||
Y_masked = Y_m[mask]
|
||||
Z_masked = Z_nm[mask]
|
||||
|
||||
freqs, psd = compute_surface_psd(X_masked, Y_masked, Z_masked, 0.575)
|
||||
rms_total = np.sqrt(np.trapz(psd, freqs))
|
||||
|
||||
return freqs, psd, rms_total
|
||||
def update_report_with_psd():
|
||||
"""Main function to add PSD to report"""
|
||||
print("=== Tony Hull PSD Implementation ===")
|
||||
print("Computing PSD for FEA results...")
|
||||
|
||||
op2_path = "/home/papa/obsidian-vault/2-Projects/P04-GigaBIT-M1/Technical-Analysis/Tensor_Project/assy_m1_assyfem1_sim1-solution_1.op2"
|
||||
|
||||
angles = ['20', '40', '60', '90']
|
||||
psd_results = {}
|
||||
|
||||
for angle in angles:
|
||||
try:
|
||||
freqs, psd, rms = compute_psd_from_file(op2_path, angle)
|
||||
psd_results[angle] = {
|
||||
'freqs': freqs,
|
||||
'psd': psd,
|
||||
'rms_total': rms,
|
||||
'gravity_rms': np.sqrt(np.trapz(psd[(freqs >= 0.1) & (freqs <= 2)], freqs[(freqs >= 0.1) & (freqs <= 2)])) if np.any((freqs >= 0.1) & (freqs <= 2)) else 0,
|
||||
'support_rms': np.sqrt(np.trapz(psd[(freqs >= 2) & (freqs <= 20)], freqs[(freqs >= 2) & (freqs <= 20)])) if np.any((freqs >= 2) & (freqs <= 20)) else 0
|
||||
}
|
||||
print(f" {angle}°: RMS={rms:.2f}nm Gravity={psd_results[angle]['gravity_rms']:.2f}nm Support={psd_results[angle]['support_rms']:.2f}nm")
|
||||
except Exception as e:
|
||||
print(f" Error processing {angle}°: {e}")
|
||||
|
||||
print(f"\nTony Hull PSD complete for {len(psd_results)} angles")
|
||||
return psd_results
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = update_report_with_psd()
|
||||
Reference in New Issue
Block a user