Source code for prescription.sections.surface_table

"""Surface geometry and material table sections.

Kramer Harrison, 2026
"""

from __future__ import annotations

import math
from typing import TYPE_CHECKING

from optiland.prescription._formatting import fmt_float, fmt_infinity, safe_eval
from optiland.prescription.document import Section, TableBlock, TextBlock
from optiland.prescription.sections.base import BaseSection

if TYPE_CHECKING:
    from optiland.optic.optic import Optic
    from optiland.surfaces.standard_surface import Surface

_FREEFORM_FOOTNOTE = (
    "† Freeform geometry: radius column shows geometry type. "
    "Full parameters available via optic.surfaces.surfaces[i].geometry."
)


def _geometry_type_tag(surface: Surface) -> str:
    geom = surface.geometry
    geom_name = type(geom).__name__
    # Sag count for Zernike-type
    if hasattr(geom, "num_terms"):
        return f"[{geom_name}({geom.num_terms})]"
    return f"[{geom_name}]"


def _radius_str(surface: Surface) -> tuple[str, bool]:
    """Return (radius_string, is_freeform)."""
    geom = surface.geometry
    if not hasattr(geom, "radius"):
        return _geometry_type_tag(surface), True
    try:
        r = float(geom.radius)
    except (TypeError, ValueError):
        return _geometry_type_tag(surface), True
    if math.isinf(r) or abs(r) > 1e12:
        return "∞", False
    return fmt_float(r), False


def _conic_str(surface: Surface) -> str:
    geom = surface.geometry
    if hasattr(geom, "k"):
        return fmt_float(geom.k)
    return "—"


[docs] class SurfaceGeometryTableSection(BaseSection): """Produces the 'Surface Data — Geometry' section."""
[docs] def build(self, optic: Optic) -> Section: """Build the Surface Geometry table section. Args: optic: The optical system to describe. Returns: Section containing a TableBlock of surface geometry data. """ surfaces = optic.surfaces.surfaces rows = [] has_freeform = False for i, s in enumerate(surfaces): radius_str, is_freeform = _radius_str(s) if is_freeform: has_freeform = True radius_str = radius_str + " †" thick_str = fmt_infinity(s.thickness) conic_str = _conic_str(s) stop_str = "✓" if getattr(s, "is_stop", False) else "" comment = getattr(s, "comment", "") or "" surf_type = getattr(s, "surface_type", None) or type(s.geometry).__name__ rows.append( [str(i), surf_type, radius_str, thick_str, conic_str, stop_str, comment] ) table = TableBlock( headers=[ "S#", "Type", "Radius (mm)", "Thickness (mm)", "Conic", "Stop", "Comment", ], rows=rows, ) blocks = [table] if has_freeform: blocks.append(TextBlock(text=_FREEFORM_FOOTNOTE)) return Section(title="Surface Data — Geometry", blocks=blocks)
[docs] class SurfaceMaterialTableSection(BaseSection): """Produces the 'Surface Data — Materials' section."""
[docs] def build(self, optic: Optic) -> Section: """Build the Surface Materials table section. Args: optic: The optical system to describe. Returns: Section containing a TableBlock of surface material data. """ surfaces = optic.surfaces.surfaces rows = [] for i, s in enumerate(surfaces): mat_name = _material_name(s) nd_str = _nd_str(s) vd_str = _vd_str(s) semi_str = _semi_aperture_str(s) coat_str = _coating_str(s) rows.append([str(i), mat_name, nd_str, vd_str, semi_str, coat_str]) table = TableBlock( headers=["S#", "Material", "nd", "Vd", "Semi-Diameter (mm)", "Coating"], rows=rows, ) return Section(title="Surface Data — Materials", blocks=[table])
def _material_name(surface: Surface) -> str: im = surface.interaction_model if getattr(im, "is_reflective", False): return "MIRROR" mat = surface.material_post mat_type = type(mat).__name__ if mat_type == "IdealMaterial": try: nd = float(mat.n(0.5876)) except Exception: nd = 1.0 if abs(nd - 1.0) < 1e-6: return "Air" return f"Ideal (n={nd:.4f})" name = getattr(mat, "name", None) if name: return str(name) return mat_type def _nd_str(surface: Surface) -> str: im = surface.interaction_model if getattr(im, "is_reflective", False): return "—" mat = surface.material_post mat_type = type(mat).__name__ if mat_type == "IdealMaterial": try: nd = float(mat.n(0.5876)) except Exception: return "N/A" if abs(nd - 1.0) < 1e-6: return "1.0000" return fmt_float(nd) return safe_eval(lambda: mat.n(0.5876), infinity=False) def _vd_str(surface: Surface) -> str: im = surface.interaction_model if getattr(im, "is_reflective", False): return "—" mat = surface.material_post mat_type = type(mat).__name__ if mat_type == "IdealMaterial": return "—" if not hasattr(mat, "abbe"): return "—" return safe_eval(lambda: mat.abbe(), infinity=False) def _semi_aperture_str(surface: Surface) -> str: val = getattr(surface, "semi_aperture", None) if val is None: return "—" return safe_eval(lambda: val, infinity=False) def _coating_str(surface: Surface) -> str: try: coating = surface.interaction_model.coating except AttributeError: return "—" if coating is None: return "—" return type(coating).__name__