Source code for fields.field_group

"""Field Group Module

This module defines the FieldGroup class, which manages a collection of fields
in an optical system.

Kramer Harrison, 2024
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import optiland.backend as be
from optiland.fields.field import Field
from optiland.fields.field_types import BaseFieldDefinition

if TYPE_CHECKING:
    from optiland._types import ScalarOrArray


[docs] class FieldGroup: """A class representing a group of fields. Attributes: fields (list): A list of fields in the group. telecentric (bool): Whether the system is telecentric in object space. Methods: get_vig_factor(Hx, Hy): Returns the vignetting factors for given Hx and Hy values. get_field_coords: Returns the normalized coordinates of the fields. add_field(field): Adds a field to the group. get_field(field_number): Returns the field at the specified index. """ def __init__(self): self.fields = [] self.field_definition: BaseFieldDefinition | None = None self.telecentric = False @property def x_fields(self): """be.ndarray: x field values.""" return be.array([field.x for field in self.fields]) @property def y_fields(self): """be.ndarray: y field values.""" return be.array([field.y for field in self.fields]) @property def max_x_field(self): """float: Maximum field value in the x-direction.""" return be.max(self.x_fields) @property def max_y_field(self): """float: Maximum field value in the y-direction.""" return be.max(self.y_fields) @property def max_field(self): """float: Maximum radial field value.""" if not self.fields: return 0.0 return be.max(be.sqrt(self.x_fields**2 + self.y_fields**2)) @property def num_fields(self): """int: number of fields in field group""" return len(self.fields) def __getitem__(self, index): return self.fields[index] def __iter__(self): return iter(self.fields) def __len__(self): return len(self.fields) @property def vx(self): """be.ndarray: Vignetting factors in x for each field.""" return be.array([field.vx for field in self.fields]) @property def vy(self): """be.ndarray: Vignetting factors in y for each field.""" return be.array([field.vy for field in self.fields])
[docs] def get_vig_factor(self, Hx, Hy): """Calculates the vignetting factors for a given field position. Note that the vignetting factors are interpolated using the nearest neighbor method. Args Hx (float): The normalized x component of the field. Hy (float): The normalized y component of the field. Returns: vx_new (float): The interpolated x-component of the vignetting factor. vy_new (float): The interpolated y-component of the vignetting factor. """ max_field = self.max_field if max_field == 0: x_fields = self.x_fields y_fields = self.y_fields else: x_fields = self.x_fields / max_field y_fields = self.y_fields / max_field fields = be.stack((x_fields, y_fields), axis=-1) v_data = be.stack((self.vx, self.vy), axis=-1) result = be.nearest_nd_interpolator(fields, v_data, Hx, Hy) vx_new = result[..., 0] vy_new = result[..., 1] return vx_new, vy_new
[docs] def get_field_coords(self) -> list[tuple[ScalarOrArray, ScalarOrArray]]: """Returns the coordinates of the fields. If the maximum field size is 0, it returns a single coordinate (0, 0). Otherwise, it calculates the normalized coordinates for each field based on the maximum field size. Returns: A list of tuples, where each tuple contains the (normalized_x, normalized_y) coordinates of a field. """ max_field = self.max_field if max_field == 0: return [(0, 0)] return [ (float(x / max_field), float(y / max_field)) for x, y in zip(self.x_fields, self.y_fields, strict=False) ]
@property def weights(self) -> tuple[float, ...]: """tuple[float, ...]: Weights for all fields as a tuple.""" return tuple(field.weight for field in self.fields)
[docs] def add( self, y: float, x: float = 0.0, vx: float = 0.0, vy: float = 0.0, weight: float = 1.0, ): """Add a field to the list of fields. Args: y: The y-coordinate of the field. x: The x-coordinate of the field. Defaults to 0.0. vx: The x-component of the field's vignetting factor. Defaults to 0.0. vy: The y-component of the field's vignetting factor. Defaults to 0.0. weight: Non-negative relative importance scalar (default 1.0). A weight of 0.0 means this field is excluded from optimization and weighted analysis but is still present in standalone analysis outputs. Negative values raise ValueError. """ new_field = Field(x, y, vx, vy, weight) self.fields.append(new_field)
[docs] def set_type(self, field_type: str) -> None: """Set the type of field used in the optical system. Args: field_type: The type of field, e.g., 'angle', 'object_height', or 'paraxial_image_height'. """ self.field_definition = BaseFieldDefinition.create(field_type)
[docs] def get_field(self, field_number: int) -> Field: """Retrieve the field at the specified field_number. Args: field_number (int): The index of the field to retrieve. Returns: Field: The field at the specified index. Raises: IndexError: If the field_number is out of range. """ return self.fields[field_number]
[docs] def remove(self, field_number: int) -> None: """Remove the field at the specified field_number. Args: field_number (int): The index of the field to remove. Raises: IndexError: If the field_number is out of range. """ self.fields.pop(field_number)
[docs] def set_telecentric(self, is_telecentric): """Specify whether the system is telecentric in object space. Args: is_telecentric (bool): Whether the system is telecentric in object space. """ self.telecentric = is_telecentric
[docs] def to_dict(self): """Convert the field group to a dictionary. Returns: dict: A dictionary representation of the field group. """ data = { "fields": [field.to_dict() for field in self.fields], "telecentric": self.telecentric, "field_definition": ( self.field_definition.to_dict() if self.field_definition is not None else None ), } return data
[docs] @classmethod def from_dict(cls, data): """Create a field group from a dictionary. Args: data (dict): A dictionary representation of the field group. Returns: FieldGroup: A field group object created from the dictionary. """ field_group = cls() for field_dict in data["fields"]: field_group.add( y=field_dict.get("y", 0.0), x=field_dict.get("x", 0.0), vx=field_dict.get("vx", 0.0), vy=field_dict.get("vy", 0.0), weight=field_dict.get("weight", 1.0), ) field_group.set_telecentric(data["telecentric"]) if data.get("field_definition"): field_group.field_definition = BaseFieldDefinition.from_dict( data["field_definition"] ) return field_group