paraxial_path#
Folded-path paraxial metadata.
This module is the single source of truth for the scalar folded paraxial
model introduced by issues #726/#728: it walks the surface chain once and
records, per surface, the vertex and local axis in global coordinates, the
incoming/outgoing beam directions, the reflection parity, the signed unfolded
axial coordinate, and the orientation sign that powered surfaces need for
their paraxial-effective radius. Every consumer of folded first-order data
(SurfaceGroup.positions, the paraxial ray tracer, the ray aimers, the
pupil-point helpers) builds or receives one ParaxialPath instead of
re-deriving frames, directions and parity independently.
Supported scalar domain: piecewise-centered systems whose changes of
propagation direction are produced by plane fold mirrors, entered along an
arbitrary finite unit direction. Powered surfaces must be normal to their
local propagation segment. Anything outside that domain (oblique powered
mirrors, tilted powered refractive surfaces, refractive interfaces that steer
the nominal axis, transversely decentered vertex chains, non-object surfaces
at infinity on a folded arm) is recorded as a diagnostic and rejected with
UnsupportedParaxialGeometryError when scalar paraxial analysis or
ray aiming is requested – real ray tracing remains available for physically
valid geometries even when the scalar paraxial model is rejected.
Kramer Harrison, 2026
Functions
Angular collinearity tolerance, backend/dtype aware. |
|
|
Walk the surface chain once and assemble the shared path metadata. |
Whether a paraxial seed scope is currently active. |
|
Scope in which scalar paraxial values serve only as real-ray seeds. |
|
|
Spatial residual tolerance scaled to the system's size. |
|
Guard for operations that write axial offsets into global |
|
Reject field-angle components whose tangent is numerically singular. |
Rejection half-width (degrees) around the odd multiples of 90 degrees. |
|
|
Deterministic transverse pair |
Classes
|
Immutable per-operation snapshot of the folded-path metadata. |
|
One reason a path lies outside the supported scalar domain. |
Exceptions
Scalar paraxial values are approximate for this geometry. |
|
A geometry outside the supported scalar folded paraxial domain. |
- exception ParaxialDomainWarning[source]#
Scalar paraxial values are approximate for this geometry.
Emitted (instead of
UnsupportedParaxialGeometryError) when the out-of-domain scalar paraxial engine is used only to seed a real-ray iterative solve – the final aimed rays are verified against real traces, so the approximation never surfaces as a first-order result.
- class ParaxialPath(axial_positions: BEArray, vertices_gcs: tuple, local_z_axes_gcs: tuple, incoming_directions_gcs: tuple, outgoing_directions_gcs: tuple, parity_before: tuple, parity_after: tuple, orientation_sign: tuple, entry_direction: tuple, entry_u: tuple, entry_v: tuple, all_legs_parallel_global_z: bool, positions_are_global_z: bool, legacy_aiming_compatible: bool, is_folded_or_off_axis: bool, diagnostics: tuple, advisories: tuple = (), axis_alignments: tuple = ())[source]#
Immutable per-operation snapshot of the folded-path metadata.
All vectors are in global coordinates (GCS), stored as tuples of backend scalars.
axial_positionsis the signed unfolded axial coordinate the scalar paraxial model is written in – not a global Cartesian z (usevertices_gcsfor real-space points).- Variables:
axial_positions (BEArray) – Signed unfolded axial coordinate per surface.
vertices_gcs (tuple) – Surface vertex positions in GCS.
local_z_axes_gcs (tuple) – Surface local +z axes in GCS.
incoming_directions_gcs (tuple) – Physical beam direction arriving at each surface.
outgoing_directions_gcs (tuple) – Physical beam direction leaving each surface.
parity_before (tuple) – Reflection parity (+1/-1) before each surface.
parity_after (tuple) – Reflection parity after each surface.
orientation_sign (tuple) – Per-surface sign
s_k = parity_before * sgn(z_axis . incoming_direction)that maps authored radii and focal lengths to paraxial-effective ones.entry_direction (tuple) – Unit beam direction on the entry leg.
entry_u (tuple) – First transverse basis vector of the entry frame.
entry_v (tuple) – Second transverse basis vector of the entry frame.
all_legs_parallel_global_z (bool) – Every finite propagation leg is parallel to global +/-z (true also for retro and -z-entered systems).
positions_are_global_z (bool) –
axial_positionscoincides bit-for-bit with the global z of each vertex (entry along +z, every mirror normal on the z axis). This – notall_legs_parallel_global_z– is the capability check for operations that write axial offsets intocs.z.legacy_aiming_compatible (bool) – The historical global-z aiming branch is exactly correct: positions are global z AND the entry line runs through global x = y = 0 along +z.
is_folded_or_off_axis (bool) – Negation of
positions_are_global_z.diagnostics (tuple) – Reasons this path lies outside the supported scalar domain (empty for supported paths).
advisories (tuple) – Findings on a supported straight path that scalar first-order results silently ignore (tilted powered surfaces, interior decenters). Unlike
diagnosticsthese never gate an operation – the historical numbers are still returned – they only make the approximation visible viawarn_scalar_approximations().axis_alignments (tuple) – Per-surface alignment
z_axis . incoming_directionbetween the surface’s local +z axis and the physical beam arriving at it.|alignment| == 1(withinangular_tolerance()) identifies a centered/collinear surface; anything else is genuinely oblique.
- axial_positions: BEArray#
- effective_orientation_signs() tuple[source]#
Per-surface signs mapping authored powers to scalar-effective ones.
This is the single collinear orientation policy shared by the explicit paraxial tracer and the ray-transfer-matrix assembly (they must never disagree because they selected different power conventions):
A centered/collinear surface (
|1 - |z_axis . d_in|| <=angular_tolerance()) getss_k = parity_before * sgn(z_axis . d_in)– on straight and folded paths alike, so a physically equivalent surface authored with its local axis reversed is normalized to the same effective scalar power. The canonical default authoring always hass_k = +1, preserving historical values bit-for-bit.A genuinely oblique surface on a straight-classified path keeps the historical raw value (sign
+1); the approximation is surfaced viawarn_scalar_approximations(), never silently re-signed by a heuristic.A genuinely oblique surface on a folded/off-axis path is outside the scalar domain (
require_scalar_paraxialraises). Inside aparaxial_seed_scope()– where that rejection is downgraded to a warning because the values only seed a real-ray solve – the collinear-limit sign is kept, which is the continuous limit of the supported geometry as the tilt goes to zero.
- entry_frame() tuple[source]#
Entry frame
(anchor, axial_anchor, direction, u, v).anchoris the first physical surface’s vertex,axial_anchorits unfolded axial coordinate,directionthe unit entry direction, and(u, v)the transverse basis completing it (seetransverse_basis()for the gauge convention and its pole).
- property orientation_sign_array: BEArray#
Orientation signs as a backend array aligned with the surfaces.
- point_from_axial_offset(surface_index: int, axial_offset: ScalarOrArray, side: Literal['incoming', 'outgoing'] = 'incoming') tuple[source]#
Map an unfolded axial offset from a surface vertex to a GCS point.
The point is
r_k + parity * offset * directionwith the parity and physical direction of the selected side of surfacek, which is how a signed axial distance (anEPL/XPL-style scalar) becomes a real-space location on the correct leg.- Parameters:
surface_index – Surface the offset is measured from (negative indices allowed, as in normal sequence indexing).
axial_offset – Signed unfolded axial offset from that surface.
side – Whether to use the incoming or outgoing beam direction and parity at that surface.
- Returns:
The GCS point as an
(x, y, z)tuple of backend scalars.
- require_scalar_paraxial(operation: str = 'scalar paraxial analysis')[source]#
Reject this path if it lies outside the supported scalar domain.
Raises for direct first-order analysis. Inside a
paraxial_seed_scope()(the ray aimers’ solves, where scalar paraxial values only seed a real-ray Newton polish) the same finding is reported as aParaxialDomainWarninginstead, so aiming for e.g. slightly tilted stop mirrors keeps working – its exactness is guaranteed by the real traces, not the seed.- Parameters:
operation – Name of the requested operation, used in the message.
- Raises:
UnsupportedParaxialGeometryError – If any diagnostic was recorded and no seed scope is active.
- warn_scalar_approximations(operation: str = 'scalar paraxial analysis') None[source]#
Surface any advisories as a
ParaxialDomainWarning.Straight +z systems with tilted powered surfaces or interior decenters have always had their scalar first-order values computed as if every surface were centered and normal to the axis. That behavior (and every returned number) is unchanged; this method only makes the approximation visible instead of silent. Real ray tracing accounts for the tilt/decenter exactly.
- Parameters:
operation – Name of the requested operation, used in the message.
- class ParaxialPathDiagnostic(code: str, surface_index: int | None, measured: float | None, tolerance: float | None, message: str)[source]#
One reason a path lies outside the supported scalar domain.
- Variables:
code (str) – Stable diagnostic code (one of the module-level constants).
surface_index (int | None) – Index of the offending surface or leg, or
None.measured (float | None) – The measured value that tripped the check, or
None.tolerance (float | None) – The tolerance it was compared against, or
None.message (str) – Concise human-readable explanation.
- exception UnsupportedParaxialGeometryError[source]#
A geometry outside the supported scalar folded paraxial domain.
Raised instead of silently returning plausible-but-wrong first-order numbers. Real ray tracing (
optic.surfaces.trace) remains available for the same geometry.
- angular_tolerance() float[source]#
Angular collinearity tolerance, backend/dtype aware.
Returns the float64 intent of
1e-10widened when the active backend computes at lower precision.
- build_paraxial_path(surfaces: list) ParaxialPath[source]#
Walk the surface chain once and assemble the shared path metadata.
- Parameters:
surfaces – Ordered sequence of Surface objects (object surface first).
- Returns:
The assembled
ParaxialPath. Unsupported-geometry findings are recorded as diagnostics on the path (this function never raises for them); callpath.require_scalar_paraxial()at the boundary of any scalar first-order computation.
- position_tolerance(characteristic_scale: float) float[source]#
Spatial residual tolerance scaled to the system’s size.
- Parameters:
characteristic_scale – A length characterizing the system, e.g. the largest finite vertex coordinate magnitude.
- require_global_z_geometry(surfaces, operation: str) None[source]#
Guard for operations that write axial offsets into global
cs.z.- Parameters:
surfaces – The
SurfaceGroup(or surface sequence) to check.operation – Human-readable name of the gated operation.
- Raises:
UnsupportedParaxialGeometryError – If the unfolded axial coordinate does not coincide with global z, so a scalar z-offset mutation would move surfaces off their physical legs.
- require_nonsingular_tangent_angles(*components_deg, operation: str = 'angle-field evaluation') None[source]#
Reject field-angle components whose tangent is numerically singular.
Call before every
tan(angle)evaluation on component field angles. Angles withintangent_singularity_tolerance_deg()of an odd multiple of 90 degrees raise; everything else passes through untouched.- Parameters:
*components_deg – Angle components in degrees (scalars or arrays).
operation – Name of the calling operation, used in the message.
- Raises:
UnsupportedParaxialGeometryError – With code
SINGULAR_ANGLE_TANGENTfor the first offending component.
- tangent_singularity_tolerance_deg() float[source]#
Rejection half-width (degrees) around the odd multiples of 90 degrees.
A component field angle closer than this to
90 + k * 180degrees is rejected before its tangent is evaluated: floating-pointtanreturns a huge finite number there instead of failing, and every quantity built from it (launch points, object positions, chief-ray scales) silently loses its meaning.The width is derived from the active backend precision. The tangent’s relative conditioning error at distance
delta(radians) from the pole is approximatelyeps * (pi / 2) / delta; requiring it to stay belowsqrt(eps)– i.e. the value keeps at least half its significant digits – givesdelta >= (pi / 2) * sqrt(eps), which is90 * sqrt(eps)in degrees. For float64 this is ~1.3e-6 degrees; for float32 ~0.031 degrees. Valid nonsingular one-dimensional wide fields (89, 91, 95, 105 degrees, …) lie far outside it.
- transverse_basis(direction: tuple) tuple[tuple, tuple][source]#
Deterministic transverse pair
(u, v)completingdirection.vis global +y projected off the axis and normalized – the sagittal direction of a fold in the x-z plane. Withinangular_tolerance()of a +/-y entry that projection degenerates (the unavoidable pole of any deterministic rule), andubecomes global +x projected instead.(u, v, direction)is right-handed, and for a +z entry the pair reduces exactly to the global +x/+y axes, so field semantics continue the on-axis meaning. The gauge is deliberately a global reference – never the object surface’s orientation – so a tilted or rolled object plane cannot roll the field axes.