iactrace.camera

The Camera class and detector-side components.

Camera Class

class iactrace.camera.Camera[source]

Bases: Module

Camera for photon collection and imaging.

The Camera works in its local coordinate system. Sensor positions and rotations are relative to the camera origin (typically [0, 0, 0] for a single-sensor camera). The Telescope transforms the RayBundle into the camera frame before passing it here.

Pipeline:

rb = telescope.render(...)        # LazyRayBundle
camera.image(rb)                  # pixel image (fused, per-element fold)
camera.response_matrix(rb)        # per-source pixel response (fused)
camera.collect(rb)                # (pe_vals, pe_times, pix_id, detected)
camera.trace(rb)                  # TraceResult through the camera (diagnostics)
rb.materialise()                  # flat camera-frame RayBundle
Attributes:
sensor_groups: List of SensorGroup objects. Each group owns its pixel

layout and its detection chain (optional concentrator + gap + photodetector), so different groups can carry different cones or photodetectors. Configure a group’s chain when constructing it, or via set_concentrator() / set_photodetector() / set_gap().

__init__(sensor_groups)[source]
sensor_groups = <dataclasses._MISSING_TYPE object>
collect(ray_bundle, sensor_idx=0)[source]

Per-ray output (pe_vals, pe_times, pix_id, detected).

detected is the final liveness flag: the chain output’s alive (stayed alive through the optics, hit a sensor tile, landed on the photodetector surface) AND-ed with the pixel mask (inside a real pixel, outside the edge deadband). It is False for a ray lost anywhere along the way. Entries of pix_id / pe_times for undetected rays are meaningless and must be filtered with this mask before use; pe_vals is already zeroed there.

Materialises a LazyRayBundle: per-ray output cannot be produced incrementally.

For the path rays took through the camera, see trace().

trace(ray_bundle, sensor_idx=0)[source]

The path rays take through the camera, for diagnostics / visualization.

Returns a TraceResult, like every other tracer. This one always records, so its trajectory is never None: a camera-frame Trajectory running from the last optic, through each ray’s landing on its pixel, to the end of the detection chain – the final converging leg and the scattering inside the concentrator as one continuous path. rays is the bundle the detection chain produced, as collect() reports it.

The viz helpers take the result as-is (show_camera(camera, trajectory=camera.trace(rb))); reach for .trajectory when you want the path itself.

image(ray_bundle, sensor_idx=0)[source]

Pixel image of shape (n_sensors, *pixel_shape).

Accepts either a flat RayBundle (e.g. from Telescope.trace()) or the LazyRayBundle returned by Telescope.render(). The lazy form folds per primary-mirror element so the full ray buffer is never materialised; the eager form scatters the buffer in one call.

For the path rays took through the camera, see trace().

response_matrix(lazy_bundle, sensor_idx=0)[source]

Per-source pixel response, shape (n_sources, n_sensors, *pixel_shape).

Folds per stage-0 element instead of materialising the full ray buffer; peak memory is bounded by the matrix itself.

Requires a LazyRayBundle so the per-source structure is known. Pass telescope.render(...) directly.

set_sensor_positions(sensor_idx, positions)[source]

Set positions for sensors in a group.

set_sensor_rotations(sensor_idx, rotations)[source]

Set rotations for sensors in a group.

set_concentrator(sensor_idx, concentrator)[source]

Set/replace the concentrator on sensor group sensor_idx’s chain.

set_photodetector(sensor_idx, photodetector)[source]

Set/replace the photodetector on sensor group sensor_idx’s chain.

set_gap(sensor_idx, gap)[source]

Set the gap (upstream exit -> detector spacing) on a group’s chain.

get_info()[source]

Summary of the camera configuration.

Each sensor group reports its own geometry and detection chain, since the chain (concentrator + gap + photodetector) is owned per group.

classmethod from_yaml(filename)[source]

Load a Camera from a standalone camera YAML file.

Sensor positions are interpreted as camera-local coordinates.

Args:

filename: Path to camera YAML file.

Returns:

Camera object.

to_yaml(filename, precision=6, overwrite=True)[source]

Save camera to a standalone YAML file.

Sensor positions are written in camera-local coordinates.

Args:

filename: Output file path. precision: Number of decimal places for float values. overwrite: If True, overwrite existing file.

Returns:

Path to the saved file.

to_dict()[source]

Convert camera to a standalone configuration dictionary.

Sensor positions are in camera-local coordinates.

Sensor Groups

Sensors live in the camera-local frame and accumulate rays into pixels.

class iactrace.camera.SensorGroup[source]

Bases: Module

Abstract base class for sensor groups.

A sensor group contains N sensors at different positions/orientations that share the same pixel geometry and the same detection chain. Each group owns its DetectionChain (optional concentrator + gap + photodetector), so distinct groups in one Camera can carry different cones or photodetectors.

Attributes:

positions: Sensor positions in 3D space (N, 3) rotations: Sensor rotations as Euler angles in degrees (N, 3) chain: The per-pixel DetectionChain

applied to every pixel of this group.

positions = <dataclasses._MISSING_TYPE object>
rotations = <dataclasses._MISSING_TYPE object>
chain = <dataclasses._MISSING_TYPE object>
property n_sensors

Return number of sensors in the group.

abstractmethod get_accumulator_shape()[source]

Return the shape of the accumulator array per sensor.

abstractmethod pixel_index_and_mask(sensor_idx, x, y)[source]

Localize (x, y) to a flat pixel index plus a validity mask.

property pixel_frame_rotation
scatter(pix_id, valid, values)[source]

Sum values into the pixel accumulator by precomputed assignment.

abstractmethod in_bounds(x, y)[source]

Predicate: True for (x, y) inside the sensor’s active footprint.

with_concentrator(concentrator)[source]

Return a copy of this group with its chain’s concentrator replaced.

with_photodetector(photodetector)[source]

Return a copy of this group with its chain’s photodetector replaced.

with_gap(gap)[source]

Return a copy of this group with its chain’s gap replaced.

abstractmethod to_pixel_frame(sensor_rays, pix_id)[source]

Re-express tile-local rays in their assigned pixel’s local frame.

abstractmethod from_pixel_frame(points, pix_id)[source]

Map pixel-local points back to the tile-local frame.

The inverse of to_pixel_frame() for positions, undoing the pixel centre offset (and any grid alignment) for each ray’s assigned pixel. points is (..., n_rays, 3) and pix_id (n_rays,), so a whole recorded trajectory can be lifted out of the pixel frame in one call – which is what turns a chain trace into something drawable alongside the rest of the camera.

__init__(positions, rotations, chain)
class iactrace.camera.SquareSensorGroup[source]

Bases: SensorGroup

Square pixel sensor group.

__init__(positions, rotations, width, height, bounds, edge_width=0.0, concentrator=None, photodetector=None, gap=0.0)[source]

Square-pixel sensor group.

Args:

positions: Sensor positions, shape (N, 3) (or (3,) for one). rotations: Euler angles in degrees, same shape as positions. width: Pixel count along x (> 0). height: Pixel count along y (> 0). bounds: (x_min, x_max, y_min, y_max) in the sensor-local frame. edge_width: Dead-zone width at pixel edges (>= 0). concentrator: Optional per-pixel light concentrator (e.g. a

photodetector: Per-pixel detector response. None defaults to a

perfect flat ConstantQE.

gap: Spacing from the concentrator exit (or the entrance plane when

there is no concentrator) to the detector (>= 0).

Raises:
ValueError: on malformed shapes, non-positive width/height,

degenerate bounds, or negative edge_width.

positions = <dataclasses._MISSING_TYPE object>
rotations = <dataclasses._MISSING_TYPE object>
width = <dataclasses._MISSING_TYPE object>
height = <dataclasses._MISSING_TYPE object>
edge_width = <dataclasses._MISSING_TYPE object>
x0 = <dataclasses._MISSING_TYPE object>
y0 = <dataclasses._MISSING_TYPE object>
dx = <dataclasses._MISSING_TYPE object>
dy = <dataclasses._MISSING_TYPE object>
property bounds

Return (x_min, x_max, y_min, y_max) pixel bounds.

get_accumulator_shape()[source]

Return the shape of the accumulator array per sensor.

pixel_index_and_mask(sensor_idx, x, y)[source]

Localize (x, y) to a flat pixel index plus a validity mask.

in_bounds(x, y)[source]

Predicate: True for (x, y) inside the sensor’s active footprint.

to_pixel_frame(sensor_rays, pix_id)[source]

Re-express tile-local rays in their assigned pixel’s local frame.

from_pixel_frame(points, pix_id)[source]

Map pixel-local points back to the tile-local frame.

The inverse of to_pixel_frame() for positions, undoing the pixel centre offset (and any grid alignment) for each ray’s assigned pixel. points is (..., n_rays, 3) and pix_id (n_rays,), so a whole recorded trajectory can be lifted out of the pixel frame in one call – which is what turns a chain trace into something drawable alongside the rest of the camera.

class iactrace.camera.HexagonalSensorGroup[source]

Bases: SensorGroup

Hexagonal pixel sensor group.

__init__(positions, rotations, hex_centers, edge_width=0.0, concentrator=None, photodetector=None, gap=0.0)[source]

Hexagonal-pixel sensor group.

Args:

positions: Sensor positions, shape (N, 3) (or (3,) for one). rotations: Euler angles in degrees, same shape as positions. hex_centers: Pixel centres, shape (M, 2). The grid geometry

(size, rotation, offset, lookup table) is auto-detected from these on construction.

edge_width: Dead-zone width at pixel edges (>= 0). concentrator: Optional per-pixel light concentrator (e.g. a

photodetector: Per-pixel detector response. None defaults to a

perfect flat ConstantQE.

gap: Spacing from the concentrator exit (or the entrance plane when

there is no concentrator) to the detector (>= 0).

Raises:
ValueError: on malformed shapes, empty hex_centers, or negative

edge_width.

positions = <dataclasses._MISSING_TYPE object>
rotations = <dataclasses._MISSING_TYPE object>
hex_centers = <dataclasses._MISSING_TYPE object>
n_pixels = <dataclasses._MISSING_TYPE object>
edge_width = <dataclasses._MISSING_TYPE object>
hex_size = <dataclasses._MISSING_TYPE object>
hex_inradius = <dataclasses._MISSING_TYPE object>
grid_rotation = <dataclasses._MISSING_TYPE object>
grid_offset = <dataclasses._MISSING_TYPE object>
lookup_table = <dataclasses._MISSING_TYPE object>
q_min = <dataclasses._MISSING_TYPE object>
r_min = <dataclasses._MISSING_TYPE object>
pixel_centers_grid = <dataclasses._MISSING_TYPE object>
get_accumulator_shape()[source]

Return the shape of the accumulator array per sensor.

property pixel_frame_rotation

The detected grid rotation – see SensorGroup.pixel_frame_rotation.

pixel_index_and_mask(sensor_idx, x, y)[source]

Localize (x, y) to a flat pixel index plus a validity mask.

in_bounds(x, y)[source]

Predicate: True for (x, y) inside the sensor’s active footprint.

to_pixel_frame(sensor_rays, pix_id)[source]

Re-express tile-local rays in their assigned pixel’s local frame.

from_pixel_frame(points, pix_id)[source]

Map pixel-local points back to the tile-local frame.

The inverse of to_pixel_frame() for positions, undoing the pixel centre offset (and any grid alignment) for each ray’s assigned pixel. points is (..., n_rays, 3) and pix_id (n_rays,), so a whole recorded trajectory can be lifted out of the pixel frame in one call – which is what turns a chain trace into something drawable alongside the rest of the camera.

Detection chain

Each SensorGroup owns a DetectionChain: an optional concentrator, a gap, and a photodetector, applied to every pixel of that group. Distinct groups in one camera can therefore carry different cones or photodetectors. Configure a group’s chain at construction (concentrator / photodetector / gap arguments) or functionally via the sensor_idx-keyed Camera.set_concentrator(), Camera.set_photodetector(), and Camera.set_gap() (documented on Camera above).

A ray reaching a pixel is traced onto the photodetector’s DetectionSurface (its photocathode geometry) and then weighted by the photodetector’s response.

class iactrace.camera.DetectionChain[source]

Bases: Module

A pixel’s detection train: (optional concentrator) -> surface -> photodetector.

Every chain traces rays up to the photodetector’s own sensor surface (its photocathode geometry, surface) and hands the resulting bundle back to the photodetector, which applies its detection efficiencies (QE, window response, …). Geometry is owned by the photodetector; the chain only places it, at the detector plane detector_z set by the concentrator + gap. The chain is identical for every pixel in a SensorGroup, so it runs once over all rays at once.

Attributes:

concentrator: Optional light concentrator (cone / lightguide). photodetector: Photodetector – both the response and (via its

surface) the photocathode geometry rays are traced to.

gap: Spacing from the concentrator exit (or the entrance with no cone) to

the detector plane where the photocathode is mounted. Defaults 0.0.

concentrator = <dataclasses._MISSING_TYPE object>
photodetector = <dataclasses._MISSING_TYPE object>
gap = 0.0
with_concentrator(concentrator)[source]

Return a copy of this chain with its concentrator replaced.

with_photodetector(photodetector)[source]

Return a copy of this chain with its photodetector replaced.

with_gap(gap)[source]

Return a copy of this chain with its gap replaced.

property detector_z

-(length + gap).

Type:

Detector-plane position in the pixel-local frame

property surface

The photodetector’s sensor surface, placed at detector_z.

The photodetector owns the surface with vertex_z relative to the detector plane; this property shifts it into absolute pixel-local coordinates – the surface rays are actually traced onto. Public so diagnostics (e.g. iactrace.viz.show_sensor_chain()) can read the placed geometry.

propagate(local_rays, record_trajectory=False)[source]

Trace local_rays to the sensor surface, then hand off to the photodetector.

local_rays are in the pixel-local frame (entrance at z = 0). With a concentrator, they are delivered to surface by the concentrator’s own to_surface(). With no concentrator the rays advance straight onto the surface. The handover to the photodetector is just the resulting bundle – rays at the surface, pixel-local frame – which it weights by its own detection efficiency (reading any geometry it needs from the surface it owns). Optical path length is accumulated up to the surface (concentrator fill index on its internal leg, ray medium n on the free legs).

Returns a TraceResult; take its rays for the detected bundle. Pass record_trajectory=True to also populate its trajectory with the path through the chain (pixel-local frame): the wall-by-wall bounce path where the concentrator can report one, otherwise the straight entrance-to-landing segment. Off by default, and trajectory is then None.

__init__(concentrator, photodetector, gap=0.0)
class iactrace.camera.detector.surface.DetectionSurface[source]

Bases: Module

The sensor surface a detection chain delivers rays onto.

Args:
shape: Optional single-element core surface group giving the sag

z(x, y) in the vertex frame (element 0 is used). Mutually exclusive with curvature / conic.

vertex_z: Axial position of the surface vertex, relative to the

detector plane (0 = at the plane).

curvature: c = 1 / R. 0 -> flat. > 0 concave toward the

incoming light (bowl); < 0 convex (a dome bulging toward +z).

conic: Conic constant k (0 -> sphere). radius: Aperture radius; rays landing beyond it are dropped. None

-> unbounded.

Raises:
ValueError: on non-positive radius, or when both shape and

curvature / conic are given.

__init__(shape=None, *, vertex_z=0.0, curvature=0.0, conic=0.0, radius=None)[source]
vertex_z = <dataclasses._MISSING_TYPE object>
radius = <dataclasses._MISSING_TYPE object>
is_flat = <dataclasses._MISSING_TYPE object>
shape = <dataclasses._MISSING_TYPE object>
shifted(dz)[source]

Copy with vertex_z shifted by dz (relative -> absolute placement).

sag_fn()[source]

Return this surface’s z(x, y) sag function.

For callers (e.g. 3D visualisation) that need a plain sag callable rather than the full intersection machinery.

normals_at(points)[source]

Outward unit surface normals at the transverse positions of points.

The surface is z = vertex_z + sag(x, y), so only (x, y) matter and the result is placement-independent – angle-dependent photodetectors call this on the landing origins handed over by the chain.

stop(rays)[source]

Advance rays onto the surface (no concentrator).

Concentrators

Optional light concentrators (e.g. Winston cones) sit between the incoming rays and the photodetector. Optical path length through the guide is weighted by the concentrator’s fill index (1.0 for an air-filled cone). All cones share the PolygonalCone wall-tracing base.

class iactrace.camera.Concentrator[source]

Bases: Module

Abstract base for per-pixel light concentrators.

A concentrator funnels light from its entrance aperture (z = 0) toward its exit aperture (z = -length) in its local space, onto a stopping surface. Its one transport primitive is to_surface(): deliver rays from the entrance aperture onto a given DetectionSurface, tracing the concentrator’s own internal geometry jointly with that surface.

length = <dataclasses._MISSING_TYPE object>
abstractmethod to_surface(rays, surface)[source]

Deliver rays from the entrance aperture onto surface.

Args:

rays: Rays at the entrance aperture, pixel-local frame. surface: The stopping surface, placed in the pixel-local frame.

Returns:

Rays landed on surface, same frame.

trace_to_surface(rays, surface)[source]

to_surface(), additionally reporting the path rays took.

Returns a TraceResult whose trajectory runs through the pixel-local frame, or is None when this concentrator cannot report a path – the base implementation, which subclasses that trace internally (e.g. PolygonalCone) override. Callers fall back to a straight entrance-to-landing segment on None.

apply(rays)[source]

Transport rays to the exit aperture (a flat plane at z = -length).

Convenience for standalone concentrator use / diagnostics: to_surface() onto a flat, unbounded stop at the exit aperture.

cross_sections()[source]

Optional geometry for iactrace.viz.show_sensor_chain().

Returns (z, rings) or None:

  • z; shape (K,) axial samples, z[0] = 0 (entrance) .. z[-1] = -length (exit).

  • rings; shape (K, M, 2): the M-gon wall cross-section at each slice in the pixel-local frame (M = 6 hex, M = 4 square, large M ~ round). rings[0] is the entrance aperture, rings[-1] the exit aperture.

The default returns None (“not drawable”); concrete concentrators override it once they know their profile.

__init__(length)
class iactrace.camera.PolygonalCone[source]

Bases: Concentrator

A hollow reflective cone whose n_sides facets are lofted around a meridian: one concrete Concentrator that delivers rays by bouncing them off its reflecting walls.

  • Geometry. The facet plane normals (n_hats) and the drawable cross-sections depend only on the polygon (n_sides, orientation) and on the meridian samples the subclass provides via _meridian().

  • Wall tracing. to_surface() runs the shared trace_chain() bounce loop, which asks the cone for the raw nearest wall hit (_nearest_hit()); the cavity clamp (nearest_hit()), the mouth-aperture mask (in_mouth()) and the off-wall reflection book-keeping (reflect_ray()) are shared here. All tracer coordinates are the cone frame (exit z = 0, mouth z = length).

A new cone type therefore only describes its meridian profile (_meridian()) and its per-facet intersection (_nearest_hit()). Every field is static, so the whole cone is a leaf-free pytree the tracer can broadcast through vmap at no runtime cost.

n_sides = <dataclasses._MISSING_TYPE object>
orientation = <dataclasses._MISSING_TYPE object>
entrance_apothem = <dataclasses._MISSING_TYPE object>
exit_apothem = <dataclasses._MISSING_TYPE object>
reflectivity = <dataclasses._MISSING_TYPE object>
max_bounces = <dataclasses._MISSING_TYPE object>
property n_hats

(M, 2) inward plane normals of the polygon facets.

nearest_hit(o, d)[source]

Nearest forward wall hit for one ray, clamped to z in [0, length].

Returns (t, normal) with t = inf when the nearest wall root falls outside the cavity – e.g. the ray has dropped below the exit, where the infinite wall surface would otherwise give a spurious root.

reflect_ray(o, d, t, normal)[source]

Reflect one ray off a wall hit at parameter t.

Returns (new_origin, new_direction, path_added). The new origin is nudged just off the wall along the reflected ray so the next intersection test sees this wall behind it; the nudge lies on the outgoing ray, so it is added back to the optical path and the geometry stays exact.

in_mouth(xy)[source]

True where the transverse position xy lies inside the mouth polygon.

to_surface(rays, surface)[source]

Trace rays through the reflecting walls onto surface.

The wall-based implementation of to_surface(): the shared trace_chain() bounces rays off the cavity walls and lands them on surface, co-traced so a sensor surface peeking into the cavity is hit mid-bounce.

trace_to_surface(rays, surface)[source]

to_surface(), also returning the per-bounce wall path.

cross_sections()[source]

Optional geometry for iactrace.viz.show_sensor_chain().

Returns (z, rings) or None:

  • z; shape (K,) axial samples, z[0] = 0 (entrance) .. z[-1] = -length (exit).

  • rings; shape (K, M, 2): the M-gon wall cross-section at each slice in the pixel-local frame (M = 6 hex, M = 4 square, large M ~ round). rings[0] is the entrance aperture, rings[-1] the exit aperture.

The default returns None (“not drawable”); concrete concentrators override it once they know their profile.

__init__(length, n_sides, orientation, entrance_apothem, exit_apothem, reflectivity, max_bounces)
class iactrace.camera.WinstonCone[source]

Bases: PolygonalCone

Polygonal CPC (Winston cone) light guide.

Defined entirely by its physical dimensions; exit apothem, entrance apothem and length. The parabolic-wall tilt (s, c) that fixes the cone is computed from them at construction (see cpc_wall_tilt()). The cone answers the per-facet meridian-parabola hit (_nearest_hit()); the bounce loop is owned by the shared trace_chain().

Args:

n_sides: Number of facets (6 = hexagonal, 4 = square, …). entrance_apothem: Entrance inradius a1; the apothem **at the entrance

plane** z = length. For a truncated cone this is the actual (truncated) entry.

exit_apothem: Exit aperture inradius a2. length: Physical depth. None builds the full (untruncated) CPC and

derives the length from a1/a2; a value truncates the cone (then entrance_apothem is the entry at that depth).

reflectivity: Per-bounce wall reflectivity (scalar). max_bounces: Maximum reflections traced before a ray is absorbed. orientation_deg: Rotation of the polygon about the optical axis.

__init__(n_sides, entrance_apothem, exit_apothem, length=None, reflectivity=0.9, max_bounces=10, orientation_deg=0.0)[source]
n_sides = <dataclasses._MISSING_TYPE object>
exit_apothem = <dataclasses._MISSING_TYPE object>
entrance_apothem = <dataclasses._MISSING_TYPE object>
reflectivity = <dataclasses._MISSING_TYPE object>
max_bounces = <dataclasses._MISSING_TYPE object>
orientation = <dataclasses._MISSING_TYPE object>
length = <dataclasses._MISSING_TYPE object>
s = <dataclasses._MISSING_TYPE object>
c = <dataclasses._MISSING_TYPE object>
property k

Meridian offset a2 * (2 + s) of the wall parabola.

class iactrace.camera.OkumuraCone[source]

Bases: PolygonalCone

Okumura light collector: a polygonal cone with Bezier-curve walls.

A hollow light guide whose n_sides walls follow a quadratic or cubic Bezier meridian (Okumura 2012, arXiv:1205.3968) rather than the Winston paraboloid. Construct it either from an explicit list of interior control points or via quadratic() / cubic(), using the relative coordinates tabulated in the paper. Like the Winston cone, it answers the per-facet Bezier-meridian hit (_nearest_hit()); the bounce loop is owned by the shared trace_chain().

The control points are given in the paper’s normalized box: the exit rim is (0, 0) and the mouth is (1, 1), so a control point (r, z) has r interpolating the inradius from exit_apothem to entrance_apothem and z interpolating the axial position from the exit plane to the mouth.

Args:

n_sides: Number of facets (6 = hexagonal, 4 = square, …). entrance_apothem: Mouth inradius a1 (the apothem at z = length). exit_apothem: Exit aperture inradius a2. control_points: Interior Bezier control points in normalized

coordinates – [(P1r, P1z)] for a quadratic curve, [(P1r, P1z), (P2r, P2z)] for a cubic one. The endpoints (0, 0) (exit) and (1, 1) (mouth) are implied.

length: Physical depth. None defaults to the length of the

equivalent full Winston cone, L = (a1 + a2) * cos/sin(theta_max) with sin(theta_max) = a2 / a1 – the same L Okumura compares against, so the Okumura cone is a true drop-in for that Winston cone.

reflectivity: Per-bounce wall reflectivity (scalar). max_bounces: Maximum reflections traced before a ray is absorbed. orientation_deg: Rotation of the polygon about the optical axis.

Raises:
ValueError: if 0 < exit_apothem < entrance_apothem is violated, if

no interior control point is given, or if the control points give a non-monotonic axial profile Z(t) (an ill-defined depth).

__init__(n_sides, entrance_apothem, exit_apothem, control_points, length=None, reflectivity=0.9, max_bounces=10, orientation_deg=0.0)[source]
n_sides = <dataclasses._MISSING_TYPE object>
exit_apothem = <dataclasses._MISSING_TYPE object>
entrance_apothem = <dataclasses._MISSING_TYPE object>
length = <dataclasses._MISSING_TYPE object>
control_points = <dataclasses._MISSING_TYPE object>
r_coeffs = <dataclasses._MISSING_TYPE object>
z_coeffs = <dataclasses._MISSING_TYPE object>
reflectivity = <dataclasses._MISSING_TYPE object>
max_bounces = <dataclasses._MISSING_TYPE object>
orientation = <dataclasses._MISSING_TYPE object>
classmethod quadratic(n_sides, entrance_apothem, exit_apothem, p1, **kwargs)[source]

Build a quadratic Okumura cone from its single Bezier control point P1.

classmethod cubic(n_sides, entrance_apothem, exit_apothem, p1, p2, **kwargs)[source]

Build a cubic Okumura cone from its Bezier control points P1 and P2.

property degree

Degree of the Bezier meridian (2 = quadratic, 3 = cubic).

Photodetectors

A photodetector is the terminal element of a detection chain: it owns its sensor surface and weights each landed ray by its detection efficiency.

class iactrace.camera.PhotoDetector[source]

Bases: Module

Abstract base for photodetector response models.

A photodetector is the terminal element of a detection chain and owns two things:

  • Its surface (surface): the DetectionSurface the chain traces rays onto – by definition every photodetector has one. The base class provides the default (an unbounded flat detector at the chain’s detector plane); photodetectors with a curved / apertured photocathode override the property.

  • Its response (detect()): it receives the rays the chain has delivered onto that surface and weights values by its detection efficiency. The handover is just the RayBundle; a photodetector with an angle-dependent response reads the geometry it needs from its own surface (e.g. normals_at() at the landing positions, turned into incidence cosines with incidence_cos()).

property surface

The sensor surface, with vertex_z relative to the detector plane.

abstractmethod detect(local_rays)[source]

Weight local_rays by detection efficiency at the sensor surface.

Args:
local_rays: Rays landed on the surface, pixel-local frame (true

directions preserved; dead / undetected rays carry 0).

Returns:

Rays with photoelectron-weighted values; geometry unchanged.

outline()[source]

Optional active-area polygon (M, 2) for the diagnostic viz.

Expressed in the pixel-local frame. The default returns None (“not drawable”), in which case iactrace.viz.show_sensor_chain() falls back to the entrance-aperture footprint.

envelope()[source]

Optional 3D envelope (z, rings) for iactrace.viz.show_sensor_chain().

Mirrors cross_sections() for the detector side: a surface of revolution / lofted wall drawn around the detector plane so a physical photodetector body (e.g. a PMT’s glass front + tube) becomes visible.

  • z; shape (K,) axial samples in the pixel-local frame, with z = 0 at the photocathode (detector) plane and +z toward the incoming light. The viz offsets these to detector_z.

  • rings; shape (K, M, 2) wall cross-section at each slice (large M ~ round).

The default returns None (“no envelope drawn”); photodetectors with a physical body override it.

__init__()
class iactrace.camera.ConstantQE[source]

Bases: PhotoDetector

Flat scalar quantum efficiency with no spatial or angular structure.

The simplest photodetector and the default detector response: a single efficiency qe applied uniformly to every ray reaching the surface (the inherited flat detector at the chain’s detector plane). Use it for a measured detection efficiency you want applied as a plain scalar, or as a perfect (qe = 1) pass-through.

Args:

qe: Quantum efficiency in [0, 1].

__init__(qe=1.0)[source]
qe = <dataclasses._MISSING_TYPE object>
detect(local_rays)[source]

Weight local_rays by detection efficiency at the sensor surface.

Args:
local_rays: Rays landed on the surface, pixel-local frame (true

directions preserved; dead / undetected rays carry 0).

Returns:

Rays with photoelectron-weighted values; geometry unchanged.

class iactrace.camera.PMT[source]

Bases: PhotoDetector

A photomultiplier: a sensor surface + a cylindrical body.

A self-contained photodetector bundling everything a PMT contributes to detection, applied after the chain hands rays over to the sensor surface:

  • Geometry. The sensor surface is the sensor surface the chain traces rays onto (surface): bounded by face_radius and placed with its vertex at vertex_z (relative to the detector plane; 0 = flush with the mount, > 0 peeks toward the light, as for a domed window).

  • Efficiency. A single detection efficiency qe is applied to every ray landing on the sensor surface. Real PMT efficiencies are measured with the entrance glass in place, so qe is the whole measured number and needs no separate window term – this is the default (n_window = None).

  • Optional entrance window. Set n_window to also weight each ray by the unpolarized Fresnel transmittance at the air/window interface for its incident angle – the angular response a single measured scalar cannot capture. When you do, qe should be the intrinsic photocathode QE (the glass loss is then modelled by the Fresnel term, not folded into qe).

A FreeformSurfaceGroup sensor is supported at the Python level (pass it as surface), but – like a freeform mirror or lens surface – is not representable in YAML.

Args:
qe: Detection efficiency in [0, 1] applied at the photocathode.

Defaults to 1.0.

n_window: Refractive index of the entrance window. None (default)

applies qe alone. A value > 1 (e.g. 1.48 for borosilicate glass) additionally weights each ray by the incident-angle Fresnel transmittance through the window.

face_radius: Sensor surface aperture radius (and the body radius). surface: The sensor surface figure, a single-element

SurfaceGroup (typically an AsphericSurfaceGroup, optionally summed with a ZernikeSurfaceGroup via SumSurfaceGroup). None (default) is a flat window.

vertex_z: Axial position of the surface’s vertex, relative to the

detector plane (0 = at the plane; same convention as vertex_z).

length: Axial length of the cylindrical body behind the sensor surface.

None defaults to 2 * face_radius.

n_facets: Facets of the revolved body (48 ~ round).

Raises:
ValueError: on qe outside [0, 1], n_window <= 1, non-positive

face_radius, negative length, or n_facets < 3.

__init__(qe=1.0, *, n_window=None, face_radius, surface=None, vertex_z=0.0, length=None, n_facets=48)[source]
qe = <dataclasses._MISSING_TYPE object>
n_window = <dataclasses._MISSING_TYPE object>
face_radius = <dataclasses._MISSING_TYPE object>
shape = <dataclasses._MISSING_TYPE object>
vertex_z = <dataclasses._MISSING_TYPE object>
length = <dataclasses._MISSING_TYPE object>
n_facets = <dataclasses._MISSING_TYPE object>
detect(local_rays)[source]

Weight local_rays by detection efficiency at the sensor surface.

Args:
local_rays: Rays landed on the surface, pixel-local frame (true

directions preserved; dead / undetected rays carry 0).

Returns:

Rays with photoelectron-weighted values; geometry unchanged.

property surface

The sensor surface.

shape supplies the figure (flat by default; curved / aspheric / Zernike otherwise), placed with its vertex at vertex_z and bounded by face_radius – the exact same DetectionSurface machinery used by every other photodetector’s surface.

outline()[source]

Optional active-area polygon (M, 2) for the diagnostic viz.

Expressed in the pixel-local frame. The default returns None (“not drawable”), in which case iactrace.viz.show_sensor_chain() falls back to the entrance-aperture footprint.

envelope()[source]

Body-only cylinder for the viz: vertex_z -> vertex_z - length.

The entry window is the sensor surface, drawn separately; the body is just the tube behind it, sharing the rim circle (z = vertex_z, r = face_radius) with a flat / recessed sensor surface so the two fit without intersection. A sensor surface that bulges past its rim (a strongly domed shape) is drawn with its body starting at the mount rather than the true apex; this is a diagnostic-viz simplification only, not a tracing concern.