feat(V&V): Zernike pipeline validation - synthetic WFE generator + round-trip validator
- generate_synthetic_wfe.py: Creates synthetic OPD surfaces from known Zernike coefficients - validate_zernike_roundtrip.py: Round-trip validation (generate → fit → compare) - validation_suite/: 18 test cases (single mode, multi-mode, noisy, edge cases) - All 18 test cases pass with 0.000 nm error (clean) and <0.3 nm (10nm noise) - M1 params: 1200mm dia, 135.75mm inner radius, 50 Noll modes Project: P-Zernike-Validation (GigaBIT M1) Requested by: Adyn Miles (StarSpec) for risk reduction before M2/M3 ordering
This commit is contained in:
526
tools/generate_synthetic_wfe.py
Normal file
526
tools/generate_synthetic_wfe.py
Normal file
@@ -0,0 +1,526 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Synthetic WFE Surface Generator for Zernike Pipeline Validation
|
||||
================================================================
|
||||
|
||||
Generates synthetic Optical Path Difference (OPD) maps from user-defined
|
||||
Zernike coefficients. Used to validate the Atomizer Zernike fitting pipeline
|
||||
by creating "known truth" surfaces that can be round-tripped through the
|
||||
WFE_from_CSV_OPD tool.
|
||||
|
||||
Features:
|
||||
- Noll-indexed Zernike polynomials (standard optical convention)
|
||||
- Full-disk or annular aperture support
|
||||
- Configurable grid density, mirror diameter, noise level
|
||||
- Multiple output formats: CSV (for WFE_from_CSV_OPD), NumPy, plot
|
||||
- Preset test cases for common validation scenarios
|
||||
|
||||
Usage:
|
||||
# Single mode test (pure astigmatism)
|
||||
python generate_synthetic_wfe.py --mode single --zernike "5:100" --output test_astig.csv
|
||||
|
||||
# Multi-mode realistic mirror
|
||||
python generate_synthetic_wfe.py --mode multi --output test_multi.csv
|
||||
|
||||
# Custom coefficients
|
||||
python generate_synthetic_wfe.py --zernike "5:80,7:45,9:25,11:15" --output test_custom.csv
|
||||
|
||||
# With noise
|
||||
python generate_synthetic_wfe.py --mode multi --noise 2.0 --output test_noisy.csv
|
||||
|
||||
# Full test suite (generates all validation cases)
|
||||
python generate_synthetic_wfe.py --suite --output-dir validation_suite/
|
||||
|
||||
Output CSV format (matching WFE_from_CSV_OPD):
|
||||
x(mm), y(mm), dz(mm)
|
||||
where dz is surface displacement (OPD = 2 * dz for reflective surfaces)
|
||||
|
||||
Author: Mario (Atomizer V&V)
|
||||
Created: 2026-03-09
|
||||
Project: P-Zernike-Validation (GigaBIT M1)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from math import factorial
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
import numpy as np
|
||||
|
||||
# ============================================================================
|
||||
# Zernike Polynomial Mathematics (matching extract_zernike.py conventions)
|
||||
# ============================================================================
|
||||
|
||||
def noll_indices(j: int) -> Tuple[int, int]:
|
||||
"""Convert Noll index j to radial order n and azimuthal frequency m."""
|
||||
if j < 1:
|
||||
raise ValueError("Noll index j must be >= 1")
|
||||
count = 0
|
||||
n = 0
|
||||
while True:
|
||||
if n == 0:
|
||||
ms = [0]
|
||||
elif n % 2 == 0:
|
||||
ms = [0] + [m for k in range(1, n // 2 + 1) for m in (-2 * k, 2 * k)]
|
||||
else:
|
||||
ms = [m for k in range(0, (n + 1) // 2) for m in (-(2 * k + 1), (2 * k + 1))]
|
||||
for m in ms:
|
||||
count += 1
|
||||
if count == j:
|
||||
return n, m
|
||||
n += 1
|
||||
|
||||
|
||||
def zernike_radial(n: int, m: int, r: np.ndarray) -> np.ndarray:
|
||||
"""Compute radial component R_n^m(r)."""
|
||||
R = np.zeros_like(r)
|
||||
m_abs = abs(m)
|
||||
for s in range((n - m_abs) // 2 + 1):
|
||||
coef = ((-1) ** s * factorial(n - s) /
|
||||
(factorial(s) *
|
||||
factorial((n + m_abs) // 2 - s) *
|
||||
factorial((n - m_abs) // 2 - s)))
|
||||
R += coef * r ** (n - 2 * s)
|
||||
return R
|
||||
|
||||
|
||||
def zernike_noll(j: int, r: np.ndarray, theta: np.ndarray) -> np.ndarray:
|
||||
"""Evaluate Noll-indexed Zernike polynomial Z_j(r, theta)."""
|
||||
n, m = noll_indices(j)
|
||||
R = zernike_radial(n, m, r)
|
||||
if m == 0:
|
||||
return R
|
||||
elif m > 0:
|
||||
return R * np.cos(m * theta)
|
||||
else:
|
||||
return R * np.sin(-m * theta)
|
||||
|
||||
|
||||
def zernike_name(j: int) -> str:
|
||||
"""Get common optical name for Zernike mode."""
|
||||
n, m = noll_indices(j)
|
||||
names = {
|
||||
(0, 0): "Piston",
|
||||
(1, -1): "Tilt X", (1, 1): "Tilt Y",
|
||||
(2, 0): "Defocus",
|
||||
(2, -2): "Astigmatism 45°", (2, 2): "Astigmatism 0°",
|
||||
(3, -1): "Coma X", (3, 1): "Coma Y",
|
||||
(3, -3): "Trefoil X", (3, 3): "Trefoil Y",
|
||||
(4, 0): "Primary Spherical",
|
||||
(4, -2): "2nd Astig X", (4, 2): "2nd Astig Y",
|
||||
(4, -4): "Quadrafoil X", (4, 4): "Quadrafoil Y",
|
||||
(5, -1): "2nd Coma X", (5, 1): "2nd Coma Y",
|
||||
(5, -3): "2nd Trefoil X", (5, 3): "2nd Trefoil Y",
|
||||
(5, -5): "Pentafoil X", (5, 5): "Pentafoil Y",
|
||||
(6, 0): "2nd Spherical",
|
||||
}
|
||||
if (n, m) in names:
|
||||
return names[(n, m)]
|
||||
return f"Z({n},{m:+d})"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Surface Generation
|
||||
# ============================================================================
|
||||
|
||||
def generate_grid(
|
||||
diameter_mm: float = 1200.0,
|
||||
inner_radius_mm: float = 135.75,
|
||||
n_points_radial: int = 200,
|
||||
grid_type: str = "cartesian",
|
||||
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Generate a 2D grid of points within the mirror aperture.
|
||||
|
||||
Returns:
|
||||
x_mm, y_mm: Physical coordinates in mm
|
||||
mask: Boolean array (True = inside aperture)
|
||||
"""
|
||||
outer_radius = diameter_mm / 2.0
|
||||
|
||||
if grid_type == "cartesian":
|
||||
# Regular Cartesian grid
|
||||
n = n_points_radial * 2
|
||||
x_1d = np.linspace(-outer_radius, outer_radius, n)
|
||||
y_1d = np.linspace(-outer_radius, outer_radius, n)
|
||||
x_mm, y_mm = np.meshgrid(x_1d, y_1d)
|
||||
x_mm = x_mm.ravel()
|
||||
y_mm = y_mm.ravel()
|
||||
elif grid_type == "scattered":
|
||||
# Random scattered points (simulates real mesh)
|
||||
n_total = (n_points_radial * 2) ** 2
|
||||
rng = np.random.default_rng(42)
|
||||
x_mm = rng.uniform(-outer_radius, outer_radius, n_total)
|
||||
y_mm = rng.uniform(-outer_radius, outer_radius, n_total)
|
||||
else:
|
||||
raise ValueError(f"Unknown grid_type: {grid_type}")
|
||||
|
||||
# Apply aperture mask
|
||||
r = np.sqrt(x_mm**2 + y_mm**2)
|
||||
if inner_radius_mm > 0:
|
||||
mask = (r <= outer_radius) & (r >= inner_radius_mm)
|
||||
else:
|
||||
mask = r <= outer_radius
|
||||
|
||||
return x_mm[mask], y_mm[mask], mask
|
||||
|
||||
|
||||
def synthesize_surface(
|
||||
x_mm: np.ndarray,
|
||||
y_mm: np.ndarray,
|
||||
coefficients: Dict[int, float],
|
||||
diameter_mm: float = 1200.0,
|
||||
noise_rms_nm: float = 0.0,
|
||||
seed: int = 42,
|
||||
) -> Tuple[np.ndarray, Dict]:
|
||||
"""
|
||||
Generate a synthetic OPD surface from Zernike coefficients.
|
||||
|
||||
Args:
|
||||
x_mm, y_mm: Physical coordinates in mm
|
||||
coefficients: Dict of {Noll_index: amplitude_nm}
|
||||
diameter_mm: Mirror diameter in mm
|
||||
noise_rms_nm: RMS of Gaussian noise to add (nm)
|
||||
seed: Random seed for reproducibility
|
||||
|
||||
Returns:
|
||||
opd_nm: OPD values in nanometers at each point
|
||||
metadata: Dict with ground truth info
|
||||
"""
|
||||
outer_radius = diameter_mm / 2.0
|
||||
|
||||
# Normalize to unit disk
|
||||
r_norm = np.sqrt(x_mm**2 + y_mm**2) / outer_radius
|
||||
theta = np.arctan2(y_mm, x_mm)
|
||||
|
||||
# Build surface from Zernike modes
|
||||
opd_nm = np.zeros_like(x_mm)
|
||||
for j, amp_nm in coefficients.items():
|
||||
Z_j = zernike_noll(j, r_norm, theta)
|
||||
opd_nm += amp_nm * Z_j
|
||||
|
||||
# Compute ground truth RMS (before noise)
|
||||
rms_total = np.sqrt(np.mean(opd_nm**2))
|
||||
|
||||
# Add noise if requested
|
||||
if noise_rms_nm > 0:
|
||||
rng = np.random.default_rng(seed)
|
||||
noise = rng.normal(0, noise_rms_nm, size=opd_nm.shape)
|
||||
opd_nm += noise
|
||||
|
||||
rms_with_noise = np.sqrt(np.mean(opd_nm**2))
|
||||
|
||||
metadata = {
|
||||
"input_coefficients": {str(j): amp for j, amp in coefficients.items()},
|
||||
"coefficient_names": {str(j): zernike_name(j) for j in coefficients},
|
||||
"n_points": len(x_mm),
|
||||
"diameter_mm": diameter_mm,
|
||||
"rms_nm_clean": float(rms_total),
|
||||
"rms_nm_with_noise": float(rms_with_noise),
|
||||
"noise_rms_nm": noise_rms_nm,
|
||||
"seed": seed,
|
||||
}
|
||||
|
||||
return opd_nm, metadata
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Output Formats
|
||||
# ============================================================================
|
||||
|
||||
def write_csv_opd(
|
||||
filepath: str,
|
||||
x_mm: np.ndarray,
|
||||
y_mm: np.ndarray,
|
||||
opd_nm: np.ndarray,
|
||||
opd_unit: str = "nm",
|
||||
):
|
||||
"""
|
||||
Write OPD surface to CSV format compatible with WFE_from_CSV_OPD.
|
||||
|
||||
Default format: x(mm), y(mm), opd(nm)
|
||||
Can also output in mm for displacement: x(mm), y(mm), dz(mm)
|
||||
|
||||
NOTE: Update this function once the exact WFE_from_CSV_OPD format is confirmed.
|
||||
"""
|
||||
filepath = Path(filepath)
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if opd_unit == "nm":
|
||||
header = "x(mm),y(mm),opd(nm)"
|
||||
data = np.column_stack([x_mm, y_mm, opd_nm])
|
||||
elif opd_unit == "mm":
|
||||
# Convert nm to mm for displacement
|
||||
dz_mm = opd_nm / 1e6
|
||||
header = "x(mm),y(mm),dz(mm)"
|
||||
data = np.column_stack([x_mm, y_mm, dz_mm])
|
||||
else:
|
||||
raise ValueError(f"Unknown opd_unit: {opd_unit}")
|
||||
|
||||
np.savetxt(filepath, data, delimiter=",", header=header, comments="",
|
||||
fmt="%.10e")
|
||||
print(f" Written: {filepath} ({len(x_mm)} points)")
|
||||
|
||||
|
||||
def write_metadata(filepath: str, metadata: Dict):
|
||||
"""Write ground truth metadata as JSON."""
|
||||
filepath = Path(filepath)
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
print(f" Metadata: {filepath}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Preset Test Cases
|
||||
# ============================================================================
|
||||
|
||||
# Realistic M1 mirror: typical gravity-induced deformation pattern
|
||||
PRESET_REALISTIC = {
|
||||
5: 80.0, # Astigmatism 0° — dominant gravity mode
|
||||
6: 45.0, # Astigmatism 45°
|
||||
7: 30.0, # Coma X
|
||||
8: 20.0, # Coma Y
|
||||
9: 15.0, # Trefoil X
|
||||
11: 10.0, # Primary Spherical
|
||||
13: 5.0, # Secondary Astigmatism
|
||||
16: 3.0, # Secondary Coma
|
||||
22: 2.0, # Secondary Spherical
|
||||
}
|
||||
|
||||
# Single-mode test cases
|
||||
SINGLE_MODE_TESTS = {
|
||||
"Z05_astig_0deg": {5: 100.0},
|
||||
"Z06_astig_45deg": {6: 100.0},
|
||||
"Z07_coma_x": {7: 100.0},
|
||||
"Z08_coma_y": {8: 100.0},
|
||||
"Z09_trefoil_x": {9: 100.0},
|
||||
"Z10_trefoil_y": {10: 100.0},
|
||||
"Z11_spherical": {11: 100.0},
|
||||
"Z22_2nd_spherical": {22: 50.0},
|
||||
"Z37_high_order": {37: 30.0},
|
||||
}
|
||||
|
||||
# Edge case tests
|
||||
EDGE_CASE_TESTS = {
|
||||
"near_zero": {5: 0.1, 7: 0.05, 11: 0.01}, # Sub-nm coefficients
|
||||
"large_amplitude": {5: 500.0, 7: 300.0}, # Large deformation
|
||||
"many_modes": {j: 100.0 / j for j in range(5, 51)}, # All modes, decreasing
|
||||
}
|
||||
|
||||
|
||||
def generate_test_suite(
|
||||
output_dir: str,
|
||||
diameter_mm: float = 1200.0,
|
||||
inner_radius_mm: float = 135.75,
|
||||
n_points_radial: int = 200,
|
||||
):
|
||||
"""Generate the full validation test suite."""
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
all_cases = {}
|
||||
|
||||
# 1. Single-mode tests
|
||||
print("\n=== Single-Mode Tests ===")
|
||||
for name, coeffs in SINGLE_MODE_TESTS.items():
|
||||
print(f"\nGenerating: {name}")
|
||||
x, y, _ = generate_grid(diameter_mm, inner_radius_mm, n_points_radial)
|
||||
opd, meta = synthesize_surface(x, y, coeffs, diameter_mm)
|
||||
write_csv_opd(output_dir / f"{name}.csv", x, y, opd)
|
||||
write_metadata(output_dir / f"{name}_truth.json", meta)
|
||||
all_cases[name] = meta
|
||||
|
||||
# 2. Realistic multi-mode
|
||||
print("\n=== Realistic Multi-Mode ===")
|
||||
name = "realistic_gravity"
|
||||
print(f"\nGenerating: {name}")
|
||||
x, y, _ = generate_grid(diameter_mm, inner_radius_mm, n_points_radial)
|
||||
opd, meta = synthesize_surface(x, y, PRESET_REALISTIC, diameter_mm)
|
||||
write_csv_opd(output_dir / f"{name}.csv", x, y, opd)
|
||||
write_metadata(output_dir / f"{name}_truth.json", meta)
|
||||
all_cases[name] = meta
|
||||
|
||||
# 3. Noisy versions
|
||||
print("\n=== Noisy Tests ===")
|
||||
for noise_level in [1.0, 5.0, 10.0]:
|
||||
name = f"realistic_noise_{noise_level:.0f}nm"
|
||||
print(f"\nGenerating: {name}")
|
||||
x, y, _ = generate_grid(diameter_mm, inner_radius_mm, n_points_radial)
|
||||
opd, meta = synthesize_surface(x, y, PRESET_REALISTIC, diameter_mm, noise_rms_nm=noise_level)
|
||||
write_csv_opd(output_dir / f"{name}.csv", x, y, opd)
|
||||
write_metadata(output_dir / f"{name}_truth.json", meta)
|
||||
all_cases[name] = meta
|
||||
|
||||
# 4. Edge cases
|
||||
print("\n=== Edge Case Tests ===")
|
||||
for name, coeffs in EDGE_CASE_TESTS.items():
|
||||
print(f"\nGenerating: {name}")
|
||||
x, y, _ = generate_grid(diameter_mm, inner_radius_mm, n_points_radial)
|
||||
opd, meta = synthesize_surface(x, y, coeffs, diameter_mm)
|
||||
write_csv_opd(output_dir / f"{name}.csv", x, y, opd)
|
||||
write_metadata(output_dir / f"{name}_truth.json", meta)
|
||||
all_cases[name] = meta
|
||||
|
||||
# 5. Full-disk (no central hole) for comparison
|
||||
print("\n=== Full-Disk (No Hole) ===")
|
||||
name = "realistic_full_disk"
|
||||
print(f"\nGenerating: {name}")
|
||||
x, y, _ = generate_grid(diameter_mm, 0.0, n_points_radial)
|
||||
opd, meta = synthesize_surface(x, y, PRESET_REALISTIC, diameter_mm)
|
||||
meta["inner_radius_mm"] = 0.0
|
||||
write_csv_opd(output_dir / f"{name}.csv", x, y, opd)
|
||||
write_metadata(output_dir / f"{name}_truth.json", meta)
|
||||
all_cases[name] = meta
|
||||
|
||||
# 6. Scattered points (simulates real FEA mesh)
|
||||
print("\n=== Scattered Grid (FEA-like) ===")
|
||||
name = "realistic_scattered"
|
||||
print(f"\nGenerating: {name}")
|
||||
x, y, _ = generate_grid(diameter_mm, inner_radius_mm, n_points_radial, grid_type="scattered")
|
||||
opd, meta = synthesize_surface(x, y, PRESET_REALISTIC, diameter_mm)
|
||||
meta["grid_type"] = "scattered"
|
||||
write_csv_opd(output_dir / f"{name}.csv", x, y, opd)
|
||||
write_metadata(output_dir / f"{name}_truth.json", meta)
|
||||
all_cases[name] = meta
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Generated {len(all_cases)} test cases in: {output_dir}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Write suite manifest
|
||||
manifest = {
|
||||
"suite": "Zernike Pipeline Validation",
|
||||
"generated": "2026-03-09",
|
||||
"mirror_diameter_mm": diameter_mm,
|
||||
"inner_radius_mm": inner_radius_mm,
|
||||
"n_zernike_modes": 50,
|
||||
"n_points_radial": n_points_radial,
|
||||
"cases": all_cases,
|
||||
}
|
||||
manifest_path = output_dir / "suite_manifest.json"
|
||||
with open(manifest_path, 'w') as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
print(f"Manifest: {manifest_path}")
|
||||
|
||||
return all_cases
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI
|
||||
# ============================================================================
|
||||
|
||||
def parse_zernike_string(s: str) -> Dict[int, float]:
|
||||
"""Parse 'j1:amp1,j2:amp2,...' format."""
|
||||
coeffs = {}
|
||||
for pair in s.split(","):
|
||||
parts = pair.strip().split(":")
|
||||
if len(parts) != 2:
|
||||
raise ValueError(f"Invalid format: '{pair}'. Use 'j:amplitude'")
|
||||
j = int(parts[0])
|
||||
amp = float(parts[1])
|
||||
coeffs[j] = amp
|
||||
return coeffs
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate synthetic WFE surfaces for Zernike validation",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Pure astigmatism (100nm)
|
||||
python generate_synthetic_wfe.py --zernike "5:100" -o test_astig.csv
|
||||
|
||||
# Realistic multi-mode mirror
|
||||
python generate_synthetic_wfe.py --preset realistic -o test_realistic.csv
|
||||
|
||||
# Full validation suite
|
||||
python generate_synthetic_wfe.py --suite -d validation_suite/
|
||||
|
||||
# Custom with noise
|
||||
python generate_synthetic_wfe.py --zernike "5:80,7:45,11:15" --noise 2.0 -o test.csv
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--zernike", "-z", type=str,
|
||||
help="Zernike coefficients as 'j:amp_nm,...' (e.g. '5:100,7:50')")
|
||||
parser.add_argument("--preset", choices=["realistic"],
|
||||
help="Use preset coefficient set")
|
||||
parser.add_argument("--suite", action="store_true",
|
||||
help="Generate full validation test suite")
|
||||
parser.add_argument("--output", "-o", type=str, default="synthetic_wfe.csv",
|
||||
help="Output CSV file path")
|
||||
parser.add_argument("--output-dir", "-d", type=str, default="validation_suite",
|
||||
help="Output directory for --suite mode")
|
||||
parser.add_argument("--diameter", type=float, default=1200.0,
|
||||
help="Mirror diameter in mm (default: 1200)")
|
||||
parser.add_argument("--inner-radius", type=float, default=135.75,
|
||||
help="Inner radius in mm for annular aperture (default: 135.75, 0=full disk)")
|
||||
parser.add_argument("--grid-points", type=int, default=200,
|
||||
help="Number of radial grid points (default: 200, total ~ 4*N^2)")
|
||||
parser.add_argument("--noise", type=float, default=0.0,
|
||||
help="Gaussian noise RMS in nm (default: 0)")
|
||||
parser.add_argument("--grid-type", choices=["cartesian", "scattered"],
|
||||
default="cartesian",
|
||||
help="Grid type (default: cartesian)")
|
||||
parser.add_argument("--unit", choices=["nm", "mm"], default="nm",
|
||||
help="OPD output unit (default: nm)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 60)
|
||||
print("Synthetic WFE Surface Generator")
|
||||
print("Zernike Pipeline Validation — GigaBIT M1")
|
||||
print("=" * 60)
|
||||
print(f" Mirror diameter: {args.diameter} mm")
|
||||
print(f" Inner radius: {args.inner_radius} mm")
|
||||
print(f" Grid points: ~{(args.grid_points*2)**2}")
|
||||
|
||||
if args.suite:
|
||||
generate_test_suite(
|
||||
args.output_dir,
|
||||
args.diameter,
|
||||
args.inner_radius,
|
||||
args.grid_points,
|
||||
)
|
||||
else:
|
||||
# Determine coefficients
|
||||
if args.preset == "realistic":
|
||||
coeffs = PRESET_REALISTIC
|
||||
elif args.zernike:
|
||||
coeffs = parse_zernike_string(args.zernike)
|
||||
else:
|
||||
print("\nERROR: Specify --zernike, --preset, or --suite")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n Coefficients:")
|
||||
for j, amp in sorted(coeffs.items()):
|
||||
print(f" Z{j:2d} ({zernike_name(j):20s}): {amp:8.2f} nm")
|
||||
|
||||
# Generate
|
||||
x, y, _ = generate_grid(args.diameter, args.inner_radius,
|
||||
args.grid_points, args.grid_type)
|
||||
opd, meta = synthesize_surface(x, y, coeffs, args.diameter,
|
||||
noise_rms_nm=args.noise)
|
||||
|
||||
print(f"\n Points in aperture: {len(x)}")
|
||||
print(f" RMS (clean): {meta['rms_nm_clean']:.3f} nm")
|
||||
if args.noise > 0:
|
||||
print(f" RMS (noisy): {meta['rms_nm_with_noise']:.3f} nm")
|
||||
|
||||
# Write outputs
|
||||
print(f"\n Output unit: {args.unit}")
|
||||
write_csv_opd(args.output, x, y, opd, opd_unit=args.unit)
|
||||
|
||||
meta_path = Path(args.output).with_suffix('.json')
|
||||
write_metadata(str(meta_path), meta)
|
||||
|
||||
print("\nDone!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
261
tools/validate_zernike_roundtrip.py
Normal file
261
tools/validate_zernike_roundtrip.py
Normal file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Zernike Round-Trip Validator
|
||||
=============================
|
||||
|
||||
Reads a synthetic WFE CSV + its truth JSON, fits Zernike coefficients,
|
||||
and compares recovered vs input coefficients.
|
||||
|
||||
This validates that the Zernike fitting math itself is correct by
|
||||
doing a generate → fit → compare round-trip.
|
||||
|
||||
Usage:
|
||||
# Single file
|
||||
python validate_zernike_roundtrip.py validation_suite/Z05_astig_0deg.csv
|
||||
|
||||
# Full suite
|
||||
python validate_zernike_roundtrip.py --suite validation_suite/
|
||||
|
||||
Author: Mario (Atomizer V&V)
|
||||
Created: 2026-03-09
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from math import factorial
|
||||
from typing import Dict, Tuple
|
||||
import numpy as np
|
||||
from numpy.linalg import lstsq
|
||||
|
||||
# ============================================================================
|
||||
# Zernike Math (same as generate_synthetic_wfe.py)
|
||||
# ============================================================================
|
||||
|
||||
def noll_indices(j: int) -> Tuple[int, int]:
|
||||
if j < 1:
|
||||
raise ValueError("Noll index j must be >= 1")
|
||||
count = 0
|
||||
n = 0
|
||||
while True:
|
||||
if n == 0:
|
||||
ms = [0]
|
||||
elif n % 2 == 0:
|
||||
ms = [0] + [m for k in range(1, n // 2 + 1) for m in (-2 * k, 2 * k)]
|
||||
else:
|
||||
ms = [m for k in range(0, (n + 1) // 2) for m in (-(2 * k + 1), (2 * k + 1))]
|
||||
for m in ms:
|
||||
count += 1
|
||||
if count == j:
|
||||
return n, m
|
||||
n += 1
|
||||
|
||||
|
||||
def zernike_radial(n, m, r):
|
||||
R = np.zeros_like(r)
|
||||
m_abs = abs(m)
|
||||
for s in range((n - m_abs) // 2 + 1):
|
||||
coef = ((-1) ** s * factorial(n - s) /
|
||||
(factorial(s) * factorial((n + m_abs) // 2 - s) * factorial((n - m_abs) // 2 - s)))
|
||||
R += coef * r ** (n - 2 * s)
|
||||
return R
|
||||
|
||||
|
||||
def zernike_noll(j, r, theta):
|
||||
n, m = noll_indices(j)
|
||||
R = zernike_radial(n, m, r)
|
||||
if m == 0:
|
||||
return R
|
||||
elif m > 0:
|
||||
return R * np.cos(m * theta)
|
||||
else:
|
||||
return R * np.sin(-m * theta)
|
||||
|
||||
|
||||
def zernike_name(j):
|
||||
n, m = noll_indices(j)
|
||||
names = {
|
||||
(0, 0): "Piston", (1, -1): "Tilt X", (1, 1): "Tilt Y",
|
||||
(2, 0): "Defocus", (2, -2): "Astig 45°", (2, 2): "Astig 0°",
|
||||
(3, -1): "Coma X", (3, 1): "Coma Y",
|
||||
(3, -3): "Trefoil X", (3, 3): "Trefoil Y",
|
||||
(4, 0): "Spherical", (4, -2): "2ndAstig X", (4, 2): "2ndAstig Y",
|
||||
(6, 0): "2nd Spherical",
|
||||
}
|
||||
return names.get((n, m), f"Z({n},{m:+d})")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Zernike Fitting (Least Squares)
|
||||
# ============================================================================
|
||||
|
||||
def fit_zernike(x_mm, y_mm, opd_nm, diameter_mm=1200.0, n_modes=50):
|
||||
"""
|
||||
Fit Zernike coefficients to OPD data via least-squares.
|
||||
|
||||
Returns:
|
||||
coefficients: array of shape (n_modes,), Noll-indexed from j=1
|
||||
"""
|
||||
outer_radius = diameter_mm / 2.0
|
||||
r_norm = np.sqrt(x_mm**2 + y_mm**2) / outer_radius
|
||||
theta = np.arctan2(y_mm, x_mm)
|
||||
|
||||
# Build Zernike basis matrix
|
||||
Z = np.zeros((len(x_mm), n_modes))
|
||||
for j in range(1, n_modes + 1):
|
||||
Z[:, j - 1] = zernike_noll(j, r_norm, theta)
|
||||
|
||||
# Least-squares fit
|
||||
coeffs, residuals, rank, sv = lstsq(Z, opd_nm, rcond=None)
|
||||
|
||||
return coeffs
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Validation
|
||||
# ============================================================================
|
||||
|
||||
def validate_file(csv_path: str, n_modes: int = 50, diameter_mm: float = 1200.0,
|
||||
tolerance_nm: float = 0.5, verbose: bool = True):
|
||||
"""
|
||||
Validate a single synthetic WFE file.
|
||||
|
||||
Returns:
|
||||
passed: bool
|
||||
results: dict with comparison details
|
||||
"""
|
||||
csv_path = Path(csv_path)
|
||||
truth_path = csv_path.with_name(csv_path.stem + "_truth.json")
|
||||
|
||||
if not truth_path.exists():
|
||||
# Try removing any suffix before _truth
|
||||
base = csv_path.stem
|
||||
truth_path = csv_path.with_name(base + "_truth.json")
|
||||
if not truth_path.exists():
|
||||
print(f" WARNING: No truth file found for {csv_path.name}")
|
||||
return None, None
|
||||
|
||||
# Load CSV
|
||||
data = np.loadtxt(csv_path, delimiter=",", skiprows=1)
|
||||
x_mm = data[:, 0]
|
||||
y_mm = data[:, 1]
|
||||
opd_nm = data[:, 2]
|
||||
|
||||
# Load truth
|
||||
with open(truth_path) as f:
|
||||
truth = json.load(f)
|
||||
|
||||
input_coeffs = {int(k): v for k, v in truth["input_coefficients"].items()}
|
||||
|
||||
# Fit Zernike
|
||||
recovered = fit_zernike(x_mm, y_mm, opd_nm, diameter_mm, n_modes)
|
||||
|
||||
# Compare
|
||||
max_error = 0.0
|
||||
results = {"modes": {}}
|
||||
all_passed = True
|
||||
|
||||
if verbose:
|
||||
print(f"\n {'Mode':>6} {'Name':>20} {'Input(nm)':>10} {'Recovered(nm)':>14} {'Error(nm)':>10} {'Status':>8}")
|
||||
print(f" {'-'*6} {'-'*20} {'-'*10} {'-'*14} {'-'*10} {'-'*8}")
|
||||
|
||||
for j in range(1, n_modes + 1):
|
||||
input_val = input_coeffs.get(j, 0.0)
|
||||
recovered_val = recovered[j - 1]
|
||||
error = abs(recovered_val - input_val)
|
||||
max_error = max(max_error, error)
|
||||
|
||||
mode_passed = error < tolerance_nm
|
||||
if not mode_passed:
|
||||
all_passed = False
|
||||
|
||||
results["modes"][j] = {
|
||||
"input": input_val,
|
||||
"recovered": float(recovered_val),
|
||||
"error": float(error),
|
||||
"passed": mode_passed,
|
||||
}
|
||||
|
||||
# Only print modes with non-zero input or significant recovery
|
||||
if verbose and (abs(input_val) > 0.01 or abs(recovered_val) > tolerance_nm):
|
||||
status = "✅" if mode_passed else "❌"
|
||||
print(f" Z{j:>4d} {zernike_name(j):>20} {input_val:>10.3f} {recovered_val:>14.3f} {error:>10.6f} {status:>8}")
|
||||
|
||||
results["max_error_nm"] = float(max_error)
|
||||
results["all_passed"] = all_passed
|
||||
results["tolerance_nm"] = tolerance_nm
|
||||
results["n_points"] = len(x_mm)
|
||||
|
||||
if verbose:
|
||||
print(f"\n Max error: {max_error:.6f} nm")
|
||||
print(f" Tolerance: {tolerance_nm:.3f} nm")
|
||||
print(f" Result: {'✅ PASS' if all_passed else '❌ FAIL'}")
|
||||
|
||||
return all_passed, results
|
||||
|
||||
|
||||
def validate_suite(suite_dir: str, n_modes: int = 50, tolerance_nm: float = 0.5):
|
||||
"""Validate all test cases in a suite directory."""
|
||||
suite_dir = Path(suite_dir)
|
||||
csv_files = sorted(suite_dir.glob("*.csv"))
|
||||
|
||||
print(f"\nValidating {len(csv_files)} test cases in: {suite_dir}")
|
||||
print("=" * 70)
|
||||
|
||||
summary = {}
|
||||
n_pass = 0
|
||||
n_fail = 0
|
||||
n_skip = 0
|
||||
|
||||
for csv_file in csv_files:
|
||||
print(f"\n{'─'*70}")
|
||||
print(f"Test: {csv_file.name}")
|
||||
|
||||
passed, results = validate_file(csv_file, n_modes, tolerance_nm=tolerance_nm)
|
||||
|
||||
if passed is None:
|
||||
n_skip += 1
|
||||
summary[csv_file.stem] = "SKIP"
|
||||
elif passed:
|
||||
n_pass += 1
|
||||
summary[csv_file.stem] = "PASS"
|
||||
else:
|
||||
n_fail += 1
|
||||
summary[csv_file.stem] = "FAIL"
|
||||
|
||||
print(f"\n{'='*70}")
|
||||
print(f"SUITE SUMMARY")
|
||||
print(f"{'='*70}")
|
||||
print(f" PASS: {n_pass}")
|
||||
print(f" FAIL: {n_fail}")
|
||||
print(f" SKIP: {n_skip}")
|
||||
print(f" Total: {len(csv_files)}")
|
||||
print(f"\n Overall: {'✅ ALL PASSED' if n_fail == 0 else '❌ FAILURES DETECTED'}")
|
||||
|
||||
return n_fail == 0, summary
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate Zernike round-trip accuracy")
|
||||
parser.add_argument("input", nargs="?", help="CSV file or --suite directory")
|
||||
parser.add_argument("--suite", type=str, help="Validate all CSVs in directory")
|
||||
parser.add_argument("--n-modes", type=int, default=50)
|
||||
parser.add_argument("--tolerance", type=float, default=0.5,
|
||||
help="Max acceptable coefficient error in nm (default: 0.5)")
|
||||
parser.add_argument("--diameter", type=float, default=1200.0)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.suite:
|
||||
passed, summary = validate_suite(args.suite, args.n_modes, args.tolerance)
|
||||
sys.exit(0 if passed else 1)
|
||||
elif args.input:
|
||||
passed, results = validate_file(args.input, args.n_modes, args.diameter, args.tolerance)
|
||||
sys.exit(0 if passed else 1)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
118573
tools/validation_suite/Z05_astig_0deg.csv
Normal file
118573
tools/validation_suite/Z05_astig_0deg.csv
Normal file
File diff suppressed because it is too large
Load Diff
14
tools/validation_suite/Z05_astig_0deg_truth.json
Normal file
14
tools/validation_suite/Z05_astig_0deg_truth.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.90787728635817,
|
||||
"rms_nm_with_noise": 41.90787728635817,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/Z06_astig_45deg.csv
Normal file
118573
tools/validation_suite/Z06_astig_45deg.csv
Normal file
File diff suppressed because it is too large
Load Diff
14
tools/validation_suite/Z06_astig_45deg_truth.json
Normal file
14
tools/validation_suite/Z06_astig_45deg_truth.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"6": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"6": "Astigmatism 0\u00b0"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.87578387312423,
|
||||
"rms_nm_with_noise": 41.87578387312423,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/Z07_coma_x.csv
Normal file
118573
tools/validation_suite/Z07_coma_x.csv
Normal file
File diff suppressed because it is too large
Load Diff
14
tools/validation_suite/Z07_coma_x_truth.json
Normal file
14
tools/validation_suite/Z07_coma_x_truth.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"7": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"7": "Coma X"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 35.928378983095506,
|
||||
"rms_nm_with_noise": 35.928378983095506,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/Z08_coma_y.csv
Normal file
118573
tools/validation_suite/Z08_coma_y.csv
Normal file
File diff suppressed because it is too large
Load Diff
14
tools/validation_suite/Z08_coma_y_truth.json
Normal file
14
tools/validation_suite/Z08_coma_y_truth.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"8": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"8": "Coma Y"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 35.928378983095506,
|
||||
"rms_nm_with_noise": 35.928378983095506,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/Z09_trefoil_x.csv
Normal file
118573
tools/validation_suite/Z09_trefoil_x.csv
Normal file
File diff suppressed because it is too large
Load Diff
14
tools/validation_suite/Z09_trefoil_x_truth.json
Normal file
14
tools/validation_suite/Z09_trefoil_x_truth.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"9": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"9": "Trefoil X"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 36.27358028530051,
|
||||
"rms_nm_with_noise": 36.27358028530051,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/Z10_trefoil_y.csv
Normal file
118573
tools/validation_suite/Z10_trefoil_y.csv
Normal file
File diff suppressed because it is too large
Load Diff
14
tools/validation_suite/Z10_trefoil_y_truth.json
Normal file
14
tools/validation_suite/Z10_trefoil_y_truth.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"10": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"10": "Trefoil Y"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 36.27358028530051,
|
||||
"rms_nm_with_noise": 36.27358028530051,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/Z11_spherical.csv
Normal file
118573
tools/validation_suite/Z11_spherical.csv
Normal file
File diff suppressed because it is too large
Load Diff
14
tools/validation_suite/Z11_spherical_truth.json
Normal file
14
tools/validation_suite/Z11_spherical_truth.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"11": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"11": "Primary Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.33581266115739,
|
||||
"rms_nm_with_noise": 41.33581266115739,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/Z22_2nd_spherical.csv
Normal file
118573
tools/validation_suite/Z22_2nd_spherical.csv
Normal file
File diff suppressed because it is too large
Load Diff
14
tools/validation_suite/Z22_2nd_spherical_truth.json
Normal file
14
tools/validation_suite/Z22_2nd_spherical_truth.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"22": 50.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 17.392678653363138,
|
||||
"rms_nm_with_noise": 17.392678653363138,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/Z37_high_order.csv
Normal file
118573
tools/validation_suite/Z37_high_order.csv
Normal file
File diff suppressed because it is too large
Load Diff
14
tools/validation_suite/Z37_high_order_truth.json
Normal file
14
tools/validation_suite/Z37_high_order_truth.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"37": 30.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"37": "Z(8,+0)"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 9.330123545845794,
|
||||
"rms_nm_with_noise": 9.330123545845794,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/large_amplitude.csv
Normal file
118573
tools/validation_suite/large_amplitude.csv
Normal file
File diff suppressed because it is too large
Load Diff
16
tools/validation_suite/large_amplitude_truth.json
Normal file
16
tools/validation_suite/large_amplitude_truth.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 500.0,
|
||||
"7": 300.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"7": "Coma X"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 235.6361394467916,
|
||||
"rms_nm_with_noise": 235.6361394467916,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/many_modes.csv
Normal file
118573
tools/validation_suite/many_modes.csv
Normal file
File diff suppressed because it is too large
Load Diff
104
tools/validation_suite/many_modes_truth.json
Normal file
104
tools/validation_suite/many_modes_truth.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 20.0,
|
||||
"6": 16.666666666666668,
|
||||
"7": 14.285714285714286,
|
||||
"8": 12.5,
|
||||
"9": 11.11111111111111,
|
||||
"10": 10.0,
|
||||
"11": 9.090909090909092,
|
||||
"12": 8.333333333333334,
|
||||
"13": 7.6923076923076925,
|
||||
"14": 7.142857142857143,
|
||||
"15": 6.666666666666667,
|
||||
"16": 6.25,
|
||||
"17": 5.882352941176471,
|
||||
"18": 5.555555555555555,
|
||||
"19": 5.2631578947368425,
|
||||
"20": 5.0,
|
||||
"21": 4.761904761904762,
|
||||
"22": 4.545454545454546,
|
||||
"23": 4.3478260869565215,
|
||||
"24": 4.166666666666667,
|
||||
"25": 4.0,
|
||||
"26": 3.8461538461538463,
|
||||
"27": 3.7037037037037037,
|
||||
"28": 3.5714285714285716,
|
||||
"29": 3.4482758620689653,
|
||||
"30": 3.3333333333333335,
|
||||
"31": 3.225806451612903,
|
||||
"32": 3.125,
|
||||
"33": 3.0303030303030303,
|
||||
"34": 2.9411764705882355,
|
||||
"35": 2.857142857142857,
|
||||
"36": 2.7777777777777777,
|
||||
"37": 2.7027027027027026,
|
||||
"38": 2.6315789473684212,
|
||||
"39": 2.5641025641025643,
|
||||
"40": 2.5,
|
||||
"41": 2.4390243902439024,
|
||||
"42": 2.380952380952381,
|
||||
"43": 2.3255813953488373,
|
||||
"44": 2.272727272727273,
|
||||
"45": 2.2222222222222223,
|
||||
"46": 2.1739130434782608,
|
||||
"47": 2.127659574468085,
|
||||
"48": 2.0833333333333335,
|
||||
"49": 2.0408163265306123,
|
||||
"50": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"10": "Trefoil Y",
|
||||
"11": "Primary Spherical",
|
||||
"12": "2nd Astig X",
|
||||
"13": "2nd Astig Y",
|
||||
"14": "Quadrafoil X",
|
||||
"15": "Quadrafoil Y",
|
||||
"16": "2nd Coma X",
|
||||
"17": "2nd Coma Y",
|
||||
"18": "2nd Trefoil X",
|
||||
"19": "2nd Trefoil Y",
|
||||
"20": "Pentafoil X",
|
||||
"21": "Pentafoil Y",
|
||||
"22": "2nd Spherical",
|
||||
"23": "Z(6,-2)",
|
||||
"24": "Z(6,+2)",
|
||||
"25": "Z(6,-4)",
|
||||
"26": "Z(6,+4)",
|
||||
"27": "Z(6,-6)",
|
||||
"28": "Z(6,+6)",
|
||||
"29": "Z(7,-1)",
|
||||
"30": "Z(7,+1)",
|
||||
"31": "Z(7,-3)",
|
||||
"32": "Z(7,+3)",
|
||||
"33": "Z(7,-5)",
|
||||
"34": "Z(7,+5)",
|
||||
"35": "Z(7,-7)",
|
||||
"36": "Z(7,+7)",
|
||||
"37": "Z(8,+0)",
|
||||
"38": "Z(8,-2)",
|
||||
"39": "Z(8,+2)",
|
||||
"40": "Z(8,-4)",
|
||||
"41": "Z(8,+4)",
|
||||
"42": "Z(8,-6)",
|
||||
"43": "Z(8,+6)",
|
||||
"44": "Z(8,-8)",
|
||||
"45": "Z(8,+8)",
|
||||
"46": "Z(9,-1)",
|
||||
"47": "Z(9,+1)",
|
||||
"48": "Z(9,-3)",
|
||||
"49": "Z(9,+3)",
|
||||
"50": "Z(9,-5)"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 16.40426082558852,
|
||||
"rms_nm_with_noise": 16.40426082558852,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/near_zero.csv
Normal file
118573
tools/validation_suite/near_zero.csv
Normal file
File diff suppressed because it is too large
Load Diff
18
tools/validation_suite/near_zero_truth.json
Normal file
18
tools/validation_suite/near_zero_truth.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 0.1,
|
||||
"7": 0.05,
|
||||
"11": 0.01
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"7": "Coma X",
|
||||
"11": "Primary Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 0.04578284369521264,
|
||||
"rms_nm_with_noise": 0.04578284369521264,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
124981
tools/validation_suite/realistic_full_disk.csv
Normal file
124981
tools/validation_suite/realistic_full_disk.csv
Normal file
File diff suppressed because it is too large
Load Diff
31
tools/validation_suite/realistic_full_disk_truth.json
Normal file
31
tools/validation_suite/realistic_full_disk_truth.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 124980,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 40.21839241800354,
|
||||
"rms_nm_with_noise": 40.21839241800354,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42,
|
||||
"inner_radius_mm": 0.0
|
||||
}
|
||||
118573
tools/validation_suite/realistic_gravity.csv
Normal file
118573
tools/validation_suite/realistic_gravity.csv
Normal file
File diff suppressed because it is too large
Load Diff
30
tools/validation_suite/realistic_gravity_truth.json
Normal file
30
tools/validation_suite/realistic_gravity_truth.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.22340829508179,
|
||||
"rms_nm_with_noise": 41.22340829508179,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/realistic_noise_10nm.csv
Normal file
118573
tools/validation_suite/realistic_noise_10nm.csv
Normal file
File diff suppressed because it is too large
Load Diff
30
tools/validation_suite/realistic_noise_10nm_truth.json
Normal file
30
tools/validation_suite/realistic_noise_10nm_truth.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.22340829508179,
|
||||
"rms_nm_with_noise": 42.39618932696218,
|
||||
"noise_rms_nm": 10.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/realistic_noise_1nm.csv
Normal file
118573
tools/validation_suite/realistic_noise_1nm.csv
Normal file
File diff suppressed because it is too large
Load Diff
30
tools/validation_suite/realistic_noise_1nm_truth.json
Normal file
30
tools/validation_suite/realistic_noise_1nm_truth.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.22340829508179,
|
||||
"rms_nm_with_noise": 41.23235114137111,
|
||||
"noise_rms_nm": 1.0,
|
||||
"seed": 42
|
||||
}
|
||||
118573
tools/validation_suite/realistic_noise_5nm.csv
Normal file
118573
tools/validation_suite/realistic_noise_5nm.csv
Normal file
File diff suppressed because it is too large
Load Diff
30
tools/validation_suite/realistic_noise_5nm_truth.json
Normal file
30
tools/validation_suite/realistic_noise_5nm_truth.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.22340829508179,
|
||||
"rms_nm_with_noise": 41.511570286278975,
|
||||
"noise_rms_nm": 5.0,
|
||||
"seed": 42
|
||||
}
|
||||
119377
tools/validation_suite/realistic_scattered.csv
Normal file
119377
tools/validation_suite/realistic_scattered.csv
Normal file
File diff suppressed because it is too large
Load Diff
31
tools/validation_suite/realistic_scattered_truth.json
Normal file
31
tools/validation_suite/realistic_scattered_truth.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 119376,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.32177716032332,
|
||||
"rms_nm_with_noise": 41.32177716032332,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42,
|
||||
"grid_type": "scattered"
|
||||
}
|
||||
456
tools/validation_suite/suite_manifest.json
Normal file
456
tools/validation_suite/suite_manifest.json
Normal file
@@ -0,0 +1,456 @@
|
||||
{
|
||||
"suite": "Zernike Pipeline Validation",
|
||||
"generated": "2026-03-09",
|
||||
"mirror_diameter_mm": 1200.0,
|
||||
"inner_radius_mm": 135.75,
|
||||
"n_zernike_modes": 50,
|
||||
"n_points_radial": 200,
|
||||
"cases": {
|
||||
"Z05_astig_0deg": {
|
||||
"input_coefficients": {
|
||||
"5": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.90787728635817,
|
||||
"rms_nm_with_noise": 41.90787728635817,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"Z06_astig_45deg": {
|
||||
"input_coefficients": {
|
||||
"6": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"6": "Astigmatism 0\u00b0"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.87578387312423,
|
||||
"rms_nm_with_noise": 41.87578387312423,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"Z07_coma_x": {
|
||||
"input_coefficients": {
|
||||
"7": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"7": "Coma X"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 35.928378983095506,
|
||||
"rms_nm_with_noise": 35.928378983095506,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"Z08_coma_y": {
|
||||
"input_coefficients": {
|
||||
"8": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"8": "Coma Y"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 35.928378983095506,
|
||||
"rms_nm_with_noise": 35.928378983095506,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"Z09_trefoil_x": {
|
||||
"input_coefficients": {
|
||||
"9": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"9": "Trefoil X"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 36.27358028530051,
|
||||
"rms_nm_with_noise": 36.27358028530051,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"Z10_trefoil_y": {
|
||||
"input_coefficients": {
|
||||
"10": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"10": "Trefoil Y"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 36.27358028530051,
|
||||
"rms_nm_with_noise": 36.27358028530051,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"Z11_spherical": {
|
||||
"input_coefficients": {
|
||||
"11": 100.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"11": "Primary Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.33581266115739,
|
||||
"rms_nm_with_noise": 41.33581266115739,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"Z22_2nd_spherical": {
|
||||
"input_coefficients": {
|
||||
"22": 50.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 17.392678653363138,
|
||||
"rms_nm_with_noise": 17.392678653363138,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"Z37_high_order": {
|
||||
"input_coefficients": {
|
||||
"37": 30.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"37": "Z(8,+0)"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 9.330123545845794,
|
||||
"rms_nm_with_noise": 9.330123545845794,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"realistic_gravity": {
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.22340829508179,
|
||||
"rms_nm_with_noise": 41.22340829508179,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"realistic_noise_1nm": {
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.22340829508179,
|
||||
"rms_nm_with_noise": 41.23235114137111,
|
||||
"noise_rms_nm": 1.0,
|
||||
"seed": 42
|
||||
},
|
||||
"realistic_noise_5nm": {
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.22340829508179,
|
||||
"rms_nm_with_noise": 41.511570286278975,
|
||||
"noise_rms_nm": 5.0,
|
||||
"seed": 42
|
||||
},
|
||||
"realistic_noise_10nm": {
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.22340829508179,
|
||||
"rms_nm_with_noise": 42.39618932696218,
|
||||
"noise_rms_nm": 10.0,
|
||||
"seed": 42
|
||||
},
|
||||
"near_zero": {
|
||||
"input_coefficients": {
|
||||
"5": 0.1,
|
||||
"7": 0.05,
|
||||
"11": 0.01
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"7": "Coma X",
|
||||
"11": "Primary Spherical"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 0.04578284369521264,
|
||||
"rms_nm_with_noise": 0.04578284369521264,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"large_amplitude": {
|
||||
"input_coefficients": {
|
||||
"5": 500.0,
|
||||
"7": 300.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"7": "Coma X"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 235.6361394467916,
|
||||
"rms_nm_with_noise": 235.6361394467916,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"many_modes": {
|
||||
"input_coefficients": {
|
||||
"5": 20.0,
|
||||
"6": 16.666666666666668,
|
||||
"7": 14.285714285714286,
|
||||
"8": 12.5,
|
||||
"9": 11.11111111111111,
|
||||
"10": 10.0,
|
||||
"11": 9.090909090909092,
|
||||
"12": 8.333333333333334,
|
||||
"13": 7.6923076923076925,
|
||||
"14": 7.142857142857143,
|
||||
"15": 6.666666666666667,
|
||||
"16": 6.25,
|
||||
"17": 5.882352941176471,
|
||||
"18": 5.555555555555555,
|
||||
"19": 5.2631578947368425,
|
||||
"20": 5.0,
|
||||
"21": 4.761904761904762,
|
||||
"22": 4.545454545454546,
|
||||
"23": 4.3478260869565215,
|
||||
"24": 4.166666666666667,
|
||||
"25": 4.0,
|
||||
"26": 3.8461538461538463,
|
||||
"27": 3.7037037037037037,
|
||||
"28": 3.5714285714285716,
|
||||
"29": 3.4482758620689653,
|
||||
"30": 3.3333333333333335,
|
||||
"31": 3.225806451612903,
|
||||
"32": 3.125,
|
||||
"33": 3.0303030303030303,
|
||||
"34": 2.9411764705882355,
|
||||
"35": 2.857142857142857,
|
||||
"36": 2.7777777777777777,
|
||||
"37": 2.7027027027027026,
|
||||
"38": 2.6315789473684212,
|
||||
"39": 2.5641025641025643,
|
||||
"40": 2.5,
|
||||
"41": 2.4390243902439024,
|
||||
"42": 2.380952380952381,
|
||||
"43": 2.3255813953488373,
|
||||
"44": 2.272727272727273,
|
||||
"45": 2.2222222222222223,
|
||||
"46": 2.1739130434782608,
|
||||
"47": 2.127659574468085,
|
||||
"48": 2.0833333333333335,
|
||||
"49": 2.0408163265306123,
|
||||
"50": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"10": "Trefoil Y",
|
||||
"11": "Primary Spherical",
|
||||
"12": "2nd Astig X",
|
||||
"13": "2nd Astig Y",
|
||||
"14": "Quadrafoil X",
|
||||
"15": "Quadrafoil Y",
|
||||
"16": "2nd Coma X",
|
||||
"17": "2nd Coma Y",
|
||||
"18": "2nd Trefoil X",
|
||||
"19": "2nd Trefoil Y",
|
||||
"20": "Pentafoil X",
|
||||
"21": "Pentafoil Y",
|
||||
"22": "2nd Spherical",
|
||||
"23": "Z(6,-2)",
|
||||
"24": "Z(6,+2)",
|
||||
"25": "Z(6,-4)",
|
||||
"26": "Z(6,+4)",
|
||||
"27": "Z(6,-6)",
|
||||
"28": "Z(6,+6)",
|
||||
"29": "Z(7,-1)",
|
||||
"30": "Z(7,+1)",
|
||||
"31": "Z(7,-3)",
|
||||
"32": "Z(7,+3)",
|
||||
"33": "Z(7,-5)",
|
||||
"34": "Z(7,+5)",
|
||||
"35": "Z(7,-7)",
|
||||
"36": "Z(7,+7)",
|
||||
"37": "Z(8,+0)",
|
||||
"38": "Z(8,-2)",
|
||||
"39": "Z(8,+2)",
|
||||
"40": "Z(8,-4)",
|
||||
"41": "Z(8,+4)",
|
||||
"42": "Z(8,-6)",
|
||||
"43": "Z(8,+6)",
|
||||
"44": "Z(8,-8)",
|
||||
"45": "Z(8,+8)",
|
||||
"46": "Z(9,-1)",
|
||||
"47": "Z(9,+1)",
|
||||
"48": "Z(9,-3)",
|
||||
"49": "Z(9,+3)",
|
||||
"50": "Z(9,-5)"
|
||||
},
|
||||
"n_points": 118572,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 16.40426082558852,
|
||||
"rms_nm_with_noise": 16.40426082558852,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42
|
||||
},
|
||||
"realistic_full_disk": {
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 124980,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 40.21839241800354,
|
||||
"rms_nm_with_noise": 40.21839241800354,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42,
|
||||
"inner_radius_mm": 0.0
|
||||
},
|
||||
"realistic_scattered": {
|
||||
"input_coefficients": {
|
||||
"5": 80.0,
|
||||
"6": 45.0,
|
||||
"7": 30.0,
|
||||
"8": 20.0,
|
||||
"9": 15.0,
|
||||
"11": 10.0,
|
||||
"13": 5.0,
|
||||
"16": 3.0,
|
||||
"22": 2.0
|
||||
},
|
||||
"coefficient_names": {
|
||||
"5": "Astigmatism 45\u00b0",
|
||||
"6": "Astigmatism 0\u00b0",
|
||||
"7": "Coma X",
|
||||
"8": "Coma Y",
|
||||
"9": "Trefoil X",
|
||||
"11": "Primary Spherical",
|
||||
"13": "2nd Astig Y",
|
||||
"16": "2nd Coma X",
|
||||
"22": "2nd Spherical"
|
||||
},
|
||||
"n_points": 119376,
|
||||
"diameter_mm": 1200.0,
|
||||
"rms_nm_clean": 41.32177716032332,
|
||||
"rms_nm_with_noise": 41.32177716032332,
|
||||
"noise_rms_nm": 0.0,
|
||||
"seed": 42,
|
||||
"grid_type": "scattered"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user