Source code for visualization.system.mirror

"""Mirror Visualization Module

This module contains the 2D and 3D mirror visualization classes.

Kramer Harrison, 2024
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import vtk
from matplotlib.patches import Polygon

import optiland.backend as be
from optiland.physical_apertures import RadialAperture
from optiland.visualization.system.surface import Surface2D, Surface3D
from optiland.visualization.system.utils import (
    revolve_contour,
    transform,
    transform_3d,
)

if TYPE_CHECKING:
    from matplotlib.artist import Artist
    from matplotlib.axes import Axes


[docs] class Mirror2D(Surface2D): """Draw a reflective surface and its optional mechanical backing in 2D.""" def __init__( self, surface: Any, ray_extent: float, *, backing_sign: float = 1.0 ) -> None: """Set the side of the reflective surface occupied by the substrate. Args: surface: Reflective optical surface. ray_extent: Local radial drawing extent in millimeters. backing_sign: Sign of the incoming ray's local axial direction. """ super().__init__(surface, ray_extent) self.backing_sign = backing_sign
[docs] def plot( self, ax: Axes, theme: Any = None, projection: str = "YZ" ) -> dict[Artist, Mirror2D]: """Plot the mirror cross-section and a flat back at its center thickness. Args: ax: Matplotlib axes receiving the mirror artists. theme: Optional visualization theme. projection: Projection plane, either ``YZ``, ``XZ``, or ``XY``. Returns: Mapping from artists to this mirror component. """ artists = {} thickness = self.surf.mirror_thickness if ( thickness and projection in ("YZ", "XZ") and not self._is_face_on(projection) ): x, y, z = self._compute_sag(projection) front = transform(x, y, z, self.surf, is_global=False) back = transform( x, y, be.ones_like(z) * (self.backing_sign * thickness), self.surf, is_global=False, ) front_axis = front[0] if projection == "XZ" else front[1] back_axis = back[0] if projection == "XZ" else back[1] front_points = be.column_stack((front[2], front_axis)) back_points = be.column_stack((back[2], back_axis)) finite = ( be.isfinite(front[2]) & be.isfinite(front_axis) & be.isfinite(back[2]) & be.isfinite(back_axis) ) facecolor = (0.6, 0.6, 0.6, 0.35) edgecolor = "gray" if theme: facecolor = theme.parameters.get("lens.color", facecolor) edgecolor = theme.parameters.get("axes.edgecolor", edgecolor) start = None for index in range(len(finite) + 1): if index < len(finite) and bool(finite[index]): if start is None: start = index continue if start is None: continue if index - start >= 2: vertices = be.concatenate( (front_points[start:index], be.flip(back_points[start:index])) ) patch = Polygon( vertices.tolist(), closed=True, facecolor=facecolor, edgecolor=edgecolor, label="Mirror backing", ) ax.add_patch(patch) artists[patch] = self start = None artists.update(super().plot(ax, theme=theme, projection=projection)) return artists
[docs] class Mirror3D(Surface3D): """Render a reflective face and its optional mechanical substrate in 3D. Args: surface: Reflective optical surface. extent: Local radial drawing extent in millimeters. backing_sign: Sign of incoming travel along the local surface axis. """ def __init__( self, surface: Any, extent: float, *, backing_sign: float = 1.0 ) -> None: super().__init__(surface, extent) self.backing_sign = backing_sign
[docs] def plot(self, renderer: vtk.vtkRenderer, theme: Any = None, **kwargs: Any) -> None: """Add the optical face and, when requested, its back and side walls. Args: renderer: VTK renderer receiving the mirror actors. theme: Optional visualization theme. **kwargs: Ignored projection settings from the system plotter. """ is_annular = ( type(self.surf.aperture) is RadialAperture and self.surf.aperture.r_min > 0 ) if is_annular: front = self._get_asymmetric_surface() renderer.AddActor(self._configure_material(front, theme=theme)) else: super().plot(renderer, theme=theme) if not self.surf.mirror_thickness: return has_circular_aperture = self.surf.aperture is None or ( type(self.surf.aperture) is RadialAperture and not is_annular ) if self.surf.geometry.is_symmetric and has_circular_aperture: backing = self._revolved_backing() else: backing = self._gridded_backing() backing.GetProperty().SetColor(0.7, 0.7, 0.7) backing.GetProperty().SetAmbient(0.35) backing.GetProperty().SetDiffuse(0.65) renderer.AddActor(backing)
def _revolved_backing(self) -> vtk.vtkActor: """Revolve a radial back profile for a circular mirror.""" inner_radius = ( self.surf.aperture.r_min if type(self.surf.aperture) is RadialAperture else 0.0 ) outer_radius = float(self.extent) radii = be.array([outer_radius, outer_radius, inner_radius, inner_radius]) zeros = be.zeros(1) edge_sag = self.surf.geometry.sag(be.array([outer_radius]), zeros) inner_sag = self.surf.geometry.sag(be.array([inner_radius]), zeros) back_z = self.backing_sign * self.surf.mirror_thickness heights = be.concatenate((edge_sag, be.ones(2) * back_z, inner_sag)) actor = revolve_contour(be.zeros(4).tolist(), radii.tolist(), heights.tolist()) return transform_3d(actor, self.surf) def _add_quad( self, cells: vtk.vtkCellArray, ids: tuple[int, int, int, int] ) -> None: """Add one outward-facing quadrilateral to a VTK cell array.""" cells.InsertNextCell(4) for point_id in ids if self.backing_sign > 0 else reversed(ids): cells.InsertCellPoint(point_id) def _gridded_backing(self) -> vtk.vtkActor: """Build a flat back and walls along an arbitrary aperture boundary.""" x, y, z = self._compute_sag_3d() if self.surf.aperture is None: inside = be.hypot(x, y) <= self.extent else: inside = self.surf.aperture.contains(x, y) inside = inside & be.isfinite(z) valid_cells = ( inside[:-1, :-1] & inside[1:, :-1] & inside[1:, 1:] & inside[:-1, 1:] ) back_z = self.backing_sign * self.surf.mirror_thickness front_points = be.column_stack( (be.ravel(x), be.ravel(y), be.ravel(be.where(be.isfinite(z), z, 0))) ) back_points = be.column_stack( (be.ravel(x), be.ravel(y), be.ravel(be.ones_like(z) * back_z)) ) points = vtk.vtkPoints() for coordinates in be.concatenate((front_points, back_points)).tolist(): points.InsertNextPoint(*coordinates) rows, cols = x.shape back_offset = rows * cols cells = vtk.vtkCellArray() for row in range(rows - 1): for col in range(cols - 1): if not valid_cells[row, col]: continue a = row * cols + col b = a + 1 c = a + cols + 1 d = a + cols self._add_quad( cells, ( a + back_offset, b + back_offset, c + back_offset, d + back_offset, ), ) if row == 0 or not valid_cells[row - 1, col]: self._add_quad(cells, (a, b, b + back_offset, a + back_offset)) if col == cols - 2 or not valid_cells[row, col + 1]: self._add_quad(cells, (b, c, c + back_offset, b + back_offset)) if row == rows - 2 or not valid_cells[row + 1, col]: self._add_quad(cells, (c, d, d + back_offset, c + back_offset)) if col == 0 or not valid_cells[row, col - 1]: self._add_quad(cells, (d, a, a + back_offset, d + back_offset)) polydata = vtk.vtkPolyData() polydata.SetPoints(points) polydata.SetPolys(cells) mapper = vtk.vtkPolyDataMapper() mapper.SetInputData(polydata) actor = vtk.vtkActor() actor.SetMapper(mapper) return transform_3d(actor, self.surf) def _configure_material(self, actor, theme=None): """Configures the material properties of the mirror surface. Args: actor (vtkActor): The actor representing the mirror surface. """ actor.GetProperty().SetColor(1, 1, 1) actor.GetProperty().SetAmbient(0.3) actor.GetProperty().SetDiffuse(0.1) actor.GetProperty().SetSpecular(1.0) actor.GetProperty().SetSpecularPower(100) return actor