Skip to content

Optical systems ​

A collection of optical elements forms an optical system. Optical systems are used together with beams for the solve_system! function. The system types differ in how they store their objects, not in how they are solved: both run through the same tracing methods. A System keeps its objects and object groups in a vector, a StaticSystem in a tuple and flattens the groups when it is built. Currently the following types are available:

julia
└── BeamletOptics.AbstractSystem
    ├── StaticSystem
    └── System

At least 2 types have been found.

Refer to the Tutorials section for examples on how to define optical systems.

BeamletOptics.System Type

A container storing the optical elements of, i.e. a camera lens or lab setup.

Fields

  • objects: vector containing the different objects (subtypes of AbstractObject) and object groups (subtypes of AbstractObjectGroup) that are part of the system. The constructor copies the vector it is given, such that push! and delete! of the system do not change it.
source
BeamletOptics.StaticSystem Type

A static container storing the optical elements of, i.e. a camera lens or lab setup. Compared to System this way defining the system is less flexible, i.e. no elements can be added or removed after construction but it allows for more performant ray-tracing. The objects of its groups are stored when it is built: objects that are added to or removed from a group later, e.g. via push!(group, object), are not seen.

Warning

This type uses long tuples for storing the elements. This container should not be used for very large optical systems as it puts a lot of stress onto the compiler.

Fields

  • objects: vector containing the different objects that are part of the system (subtypes of AbstractObject)
source

A system is a tree in the sense of AbstractTrees: children(system) returns its objects and object groups as they were added, and the tree tools such as Leaves and print_tree reach into the groups. A System prints as this tree.

AbstractTrees.children Method
julia
AbstractTrees.children(system::System)

Returns the objects and object groups of the system as they were added, groups not flattened. See also objects(system), which flattens the groups.

source

Changing a system ​

The objects of a System can be changed after its construction, e.g. to build up a setup step by step, starting from an empty system:

julia
system = System()
push!(system, lens, mirror)   # objects or object groups
delete!(system, mirror)       # removes this object, compared by identity
pop!(system)                  # removes the last top-level object and returns it
popat!(system, 1)             # removes the first top-level object and returns it

A StaticSystem can not be changed.

Base.push! Method
julia
push!(system::System, objects::ObjectOrGroup...) -> system

Adds the objects (or object groups) at the top level of the system, such that they are traced by the following calls of solve_system!. An object that is already part of the system, directly or within a group, throws an ArgumentError; in this case nothing is added.

Beams and beam groups that were solved before do not know the new object: solve them again with solve_system!(system, beam). The system must not be changed while it is being solved.

source
Base.pop! Method
julia
pop!(system::System) -> ObjectOrGroup

Removes the last top-level object (or object group) of the system and returns it. Beams that were solved before must be solved again, see delete!(system, object).

source
Base.popat! Method
julia
popat!(system::System, i::Integer) -> ObjectOrGroup

Removes the i-th top-level object (or object group) of the system and returns it. The index counts the objects and groups as they were added, a group counts as one. An index out of bounds throws a BoundsError. Beams that were solved before must be solved again, see delete!(system, object).

source
Base.delete! Method
julia
delete!(system::System, object::ObjectOrGroup) -> system

Removes the top-level object (or object group) from the system, compared by identity. Nothing happens for an object that is not part of the system. An object within a group can not be removed on its own and throws an ArgumentError: remove the group instead.

Beams and beam groups that were solved before still end on the removed object: solve them again with solve_system!(system, beam). The system must not be changed while it is being solved.

source

Solving systems ​

Optical systems are solved non-sequentially by the solve_system! function: for every ray the solver searches the system for the object that is hit next. A component can shortcut this search for the following ray by returning a BeamletOptics.Hint. This is explained in more detail in the section: Tracing logic.

BeamletOptics.solve_system! Function
julia
solve_system!(system::AbstractSystem, beam::AbstractBeam; initialize=false, r_max=get_default_r_max(), depth_max=get_default_depth_max(), check_invariant=true, threshold=get_invariant_threshold())

Trace an AbstractBeam and all sub-beams it generates through an optical system. Every call solves the beam from its start: the beam is first reset to its untraced state with empty!, so nothing of an earlier solve is reused. A call after objects were moved, added or removed therefore gives the same result as solving a newly constructed beam. The condition to stop ray tracing is that the last beam intersection is nothing or the beam interaction is nothing. Then, the system is considered to be solved. A maximum number of rays per beam (r_max) can be specified in order to avoid infinite calculations under resonant conditions, i.e. two facing mirrors. Likewise, depth_max limits how many branching levels are explored when new sub-beams are generated (for example, by beamsplitters) so that the tree cannot grow without bound. Sub-beams beyond the depth limit are dropped from the tree.

Between two calls only the beam itself and its first ray keep their identity. The child beams and all later rays are new objects after every call: read children(beam) again instead of keeping a child. The objects of the system are not reset by default: a Detector keeps the hits of earlier solves, such that several sources solved one after another superpose on it. With initialize = true the system is initialized first (see initialize!), i.e. all its detectors are emptied. Otherwise the hits of both solves add up.

Arguments

  • system::AbstractSystem: The optical system in which the beam will be traced.

  • beam::AbstractBeam: The beam object to be traced through the system.

Keyword Arguments

  • initialize = false: if true, initialize!(system) is called before the beam is traced

  • r_max = get_default_r_max(): Maximum number of rays of each beam of the tree.

  • depth_max = get_default_depth_max(): Maximum number of branching levels explored from the root beam

  • check_invariant = true: enables or disables optical invariant checks where applicable

  • threshold = get_invariant_threshold(): limit of the complex invariant of an AstigmaticGaussianBeamlet. The Lagrange invariants of both beamlets use get_lagrange_tolerance instead

source
julia
solve_system!(system::AbstractSystem, bg::AbstractBeamGroup; progress=true, initialize=false, kwargs...)

Trace every beam of the beam group bg through the system, multithreaded over the member beams. All other kwargs are passed on to solve_system! for each beam. Every beam of the group is reset with empty! and solved from its start. The reset happens before the first beam is traced, such that a cancelled or failed solve leaves no beam with an old path.

The hits of all beams accumulate on the same Detectors, and so do the hits of earlier solves unless initialize = true.

Keyword Arguments

  • progress = true: show a progress bar once tracing has run for get_progress_threshold() seconds (default 5 s). It is only drawn if stderr is a terminal, so documentation builds, CI logs and piped output stay clean.

  • initialize = false: if true, initialize!(system) is called once, before the first beam is traced. The beams themselves are solved without initializing, otherwise each beam would discard the hits of the others.

source

Some objects store data during a solve, e.g. a Detector its hits. This data is kept between solves unless the system is initialized:

BeamletOptics.initialize! Function
julia
initialize!(x)

Brings x into the state that a new solve starts from, by discarding the data that its objects have stored during previous calls of solve_system!. x can be a System or StaticSystem, an ObjectGroup (nested groups are included) or a single object. Currently this empties all Detectors. Objects that store no such data are left unchanged. Returns nothing.

solve_system! does not call this function unless its keyword initialize is set: a Detector accumulates the hits of several solves on purpose, such that several sources solved one after another superpose. Initialize before solving again after the setup was changed, e.g. in a parameter scan. The beams are not touched, solve_system! resets them itself.

A new object type that stores data during a solve implements initialize!(object). For an AbstractDetector it calls empty!(detector).

Examples

julia
for shift in shifts
    translate_to3d!(mirror, shift)
    initialize!(system)             # instead of empty!(pd) for every detector
    solve_system!(system, beam)
    P = optical_power(pd)
end

or in one call via solve_system!(system, beam; initialize = true).

source

Configuration ​

The numerical tolerances and defaults of the solver are global settings. Each has a getter without arguments, e.g. get_sdf_miss_distance(), whose docstring is listed below. The values are Preferences.jl preferences that are read when BMO is loaded, so a new value takes effect after a restart of Julia.

GetterDefaultMeaning
get_invariant_threshold1e-6limit in [m] of the complex invariant of an astigmatic beamlet
get_lagrange_tolerance0.5relative deviation of the Lagrange invariant of a beamlet
get_sdf_surface_threshold1e-9SDF value in [m] below which a ray counts as starting on the surface
get_sdf_raymarch_eps1e-10surface distance and minimum step of the ray marching in [m]
get_sdf_miss_distance1e15path length in [m] after which a marched ray is a miss
get_internal_reflection_threshold1e-6deviation of the reflectances from 1 below which a reflection is total, dimensionless
get_line_plane_intersection_threshold1e-6magnitude of the cosine between line and plane normal below which they are parallel, dimensionless
get_orthogonality_threshold1e-10magnitude of the dot product of unit vectors that still counts as orthogonal, e.g. for beam support axes, dimensionless
get_default_r_max100rays per beam, the r_max of solve_system!
get_default_depth_max100branching levels of the beam tree, the depth_max of solve_system!
get_default_wavelength1e-6wavelength in [m]
get_default_waist1e-3beam waist radius in [m]
get_default_power1e-3beam power in [W]
get_progress_threshold5.0runtime in [s] after which a progress bar is shown

Only three of them have a setter: set_invariant_threshold!, set_lagrange_tolerance! and set_progress_threshold!. Any other preference is set by its key, which is the name of the getter without get_, via BeamletOptics.Config.set_preference!:

julia
set_lagrange_tolerance!(0.2)
BeamletOptics.Config.set_preference!("sdf_miss_distance", 1e12)   # restart Julia afterwards
BeamletOptics.Config.get_invariant_threshold Function

Returns the global configuration threshold for the paraxial optical invariant check: the limit in [m] of the complex coupling invariant h1 ⋅ u2 - h2 ⋅ u1 of an astigmatic beamlet, with the ray heights h in [m] and the dimensionless slopes u. Defaults to 1e-6. The Lagrange invariants of both beamlets use get_lagrange_tolerance.

source
BeamletOptics.Config.set_invariant_threshold! Function
julia
set_invariant_threshold!(val::Real)

Sets the global configuration threshold for the paraxial optical invariant check. This preference is persistent. Please restart Julia for this to take effect.

source
BeamletOptics.Config.get_lagrange_tolerance Function

Returns the largest relative deviation of the Lagrange invariant of a GaussianBeamlet, or of an axis of an AstigmaticGaussianBeamlet, from its value at the start of the root beam, beyond which the beamlet has left the range of its model and tracing stops. Defaults to 0.5 (configurable via the lagrange_tolerance preference, see set_lagrange_tolerance!).

source
BeamletOptics.Config.set_lagrange_tolerance! Function
julia
set_lagrange_tolerance!(val::Real)

Sets the largest relative deviation of the Lagrange invariant of a beamlet, see get_lagrange_tolerance. This preference is persistent. Please restart Julia for this to take effect.

source
BeamletOptics.Config.get_sdf_surface_threshold Function

Returns the global configuration threshold for the Signed Distance Field (SDF) surface detection: a ray whose start has an SDF value below this distance in [m] counts as starting on the surface. Defaults to 1e-9 (configurable via the sdf_surface_threshold preference).

source
BeamletOptics.Config.get_sdf_raymarch_eps Function

Returns the global configuration epsilon for the SDF ray marching algorithm, a distance in [m]: the SDF value below which the marched ray has reached the surface, and the minimum step. Defaults to 1e-10 (configurable via the sdf_raymarch_eps preference).

source
BeamletOptics.Config.get_sdf_miss_distance Function

Returns the path length in [m] after which a ray that is marched towards an SDF shape counts as a miss: a shape farther from the start of the ray than this is not hit. Defaults to 1e15 (configurable via the sdf_miss_distance preference).

source
BeamletOptics.Config.get_internal_reflection_threshold Function

Returns the global configuration threshold for total internal reflection detection, dimensionless: a reflection counts as total if the reflectances |rs|² and |rp|² both deviate from 1 by at most this value. Defaults to 1e-6 (configurable via the internal_reflection_threshold preference).

source
BeamletOptics.Config.get_line_plane_intersection_threshold Function

Returns the global configuration threshold for line-plane intersection calculations, dimensionless: a line whose direction has a dot product with the plane normal of at most this magnitude counts as parallel to the plane and does not intersect it. Defaults to 1e-6 (configurable via the line_plane_intersection_threshold preference).

source
BeamletOptics.Config.get_orthogonality_threshold Function

Returns the global configuration threshold for orthogonality checks (e.g., beam support axes), dimensionless: the largest magnitude of the dot product of two unit vectors that still counts as orthogonal. For the field E0 of a PolarizedRay it is scaled by |E0|. Defaults to 1e-10 (configurable via the orthogonality_threshold preference).

source
BeamletOptics.Config.get_default_r_max Function

Returns the default maximum number of rays (segments) per beam, the keyword r_max of solve_system!. Defaults to 100 (configurable via the default_r_max preference).

source
BeamletOptics.Config.get_default_depth_max Function

Returns the default maximum number of branching levels of the beam tree, the keyword depth_max of solve_system!: the children of a beam at level depth_max + 1 (the root beam is at level 1), e.g. those attached by a beamsplitter, are dropped. Defaults to 100 (configurable via the default_depth_max preference).

source
BeamletOptics.Config.get_default_wavelength Function

Returns the default wavelength in meters.

source
BeamletOptics.Config.get_default_waist Function

Returns the default beam waist radius in meters.

source
BeamletOptics.Config.get_default_power Function

Returns the default beam total power in Watts.

source
BeamletOptics.Config.get_progress_threshold Function

Returns the runtime in seconds after which long-running calls (solve_system! on a beam group, electric_field, intensity and optical_power on a detector) show a progress bar in the terminal. Defaults to 5 s (configurable via the progress_threshold preference, see set_progress_threshold!). Inf disables all progress bars.

source
BeamletOptics.Config.set_progress_threshold! Function
julia
set_progress_threshold!(val::Real)

Sets the runtime in seconds after which long-running calls show a progress bar, see get_progress_threshold. Use Inf to disable progress bars globally. This preference is persistent. Please restart Julia for this to take effect.

source
BeamletOptics.Config.set_preference! Function
julia
set_preference!(key::String, val)

Stores the preference key with the value val for this package via Preferences.jl (it is kept in a LocalPreferences.toml, i.e. across Julia sessions) and logs a message. The configuration constants are read when the package is loaded, hence Julia must be restarted for a new value to take effect.

source