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