"""Beam propagation tools for use outside of a simulation context.
The recommended function for most use-cases is
:func:`~finesse.tracing.tools.propagate_beam` (and
:func:`~finesse.tracing.tools.propagate_beam_astig` for astigmatic beam propagations)
which traces a beam through a specified path of a model. See :ref:`propagating_beams`
for details and examples.
"""
from __future__ import annotations
import logging
import numpy as np
from collections.abc import Callable
from ..components import Cavity
from ..gaussian import BeamParam
from ..paths import OpticalPath
from ..env import warn
from ..tracing.tree import TraceTree
from ..solutions import (
ABCDSolution,
PropagationSolution,
AstigmaticPropagationSolution,
)
from ..utilities import refractive_index
from . import cytools
from finesse.components.node import OpticalNode
LOGGER = logging.getLogger(__name__)
### Composite ABCD matrices ###
[docs]def compute_abcd(
from_node=None,
to_node=None,
via_node=None,
path=None,
direction="x",
symbolic=False,
simplify=False,
solution_name=None,
):
"""Computes the composite ABCD matrix through a given path.
By setting the argument `symbolic` to true, this method will return a symbolic
representation of the ABCD matrix rather than a numeric matrix.
Parameters
----------
from_node : :class:`.Node`
Node to trace from.
to_node : :class:`.Node`
Node to trace to.
via_node : :class:`.Node`, optional
Optional node to trace via.
path : :class:`.OpticalPath`, optional
A pre-generated path to use (produced from a call to :meth:`.Model.path`).
directon : str, optional
Direction of ABCD matrix computation (can be 'x', for tangential plane,
or 'y', for sagittal plane).
symbolic : bool, tuple(Parameters), optional; default: False
If False a numerical ABCD propagation is computed. If True, a symbolic
ABCD propagation is calculated instead. A tuple of parameters can also
be provided, in this case these parameters will be kept symbolic
simplify : bool, optional
Attempt to simplify symbolic equations, can be slow for complex models
Returns
-------
out : :class:`.ABCDSolution`
ABCD matrix solution object between the specified nodes.
"""
path = _make_path(from_node, to_node, via_node, path)
# Make a tree from the forward path...
t_initial = TraceTree.from_path(path.nodes)
if t_initial is None:
raise ValueError("Cannot compute ABCD matrix from a node to itself!")
# ... then get the last branch as full ABCD is
# computed from multiplying each ABCD upwards
# through the full tree (i.e. correct multiplication order)
t = t_initial.get_last_left_branch()
t.node._model._update_symbolic_abcds()
M = handle_symbolic(
t,
direction,
simplify=simplify,
symbolic=symbolic,
sym_func=cytools.compute_symbolic_abcd,
num_func=cytools.compute_numeric_abcd,
)
fn = path.nodes[0]
tn = path.nodes[-1]
# comp1 = fn.component
# comp2 = tn.component
# # Workaround for ABCD at single connector, reverting minus sign applied
# # due to co-ordinate system transformation on reflection
# if (
# comp1 is comp2
# and direction == "x"
# and comp1.is_valid_coupling(fn, tn)
# and comp1.interaction_type(fn, tn) == InteractionType.REFLECTION
# ):
# M *= -1
if solution_name is None:
solution_name = f"ABCD_{fn.full_name}_{tn.full_name}_{direction}"
if symbolic:
solution_name += "_sym"
return ABCDSolution(solution_name, M, direction, symbolic)
### Accumulated Gouy phases ###
[docs]def acc_gouy(
from_node=None,
to_node=None,
via_node=None,
path=None,
q_in=None,
direction="x",
symbolic=False,
deg=True,
**kwargs,
):
"""Computes the accumulated Gouy phase along a specified path.
By setting the argument `symbolic` to true, this method will return a symbolic
representation of the accumulated Gouy phase rather than a number.
If the argument `q_in` is not specified then this value will be determined
from a call to :meth:`.Model.beam_trace`. Arguments to this beam trace call can be
passed via the `kwargs` of this method.
Parameters
----------
from_node : :class:`.Node`
Node to trace from.
to_node : :class:`.Node`
Node to trace to.
via_node : :class:`.Node`, optional
Optional node to trace via.
path : :class:`.OpticalPath`, optional
A pre-generated path to use.
q_in : :class:`.BeamParam`, complex, optional
Beam parameter to use at starting node. If not specified then
this will be determined from a beam trace. Note that, if specified,
this can also be a symbolic beam parameter.
direction : str, optional; default: "x"
Plane of computation (can be 'x', 'y' or `None`).
symbolic : bool, optional; default: False
Flag determining whether to return a symbolic representation.
degrees : bool, optional; default: True
Flag determining whether to convert return value from radians to degrees.
"""
path = _make_path(from_node, to_node, via_node, path)
t = TraceTree.from_path(path.nodes)
if t is None:
raise ValueError(
"Cannot calculate accumulated Gouy phase from a node to itself!"
)
q_in = _make_input_q(q_in, t.node, direction, **kwargs)
if q_in.wavelength != t.node._model.lambda0:
warn(
f"In acc_gouy:\n"
f" Wavelength of input beam parameter ({q_in.wavelength} m) not equal "
f"to wavelength of model associated with path ({t.node._model.lambda0} m)."
)
if q_in.symbolic and not symbolic:
LOGGER.info(
"In acc_gouy:\n"
" Specified q_in argument is symbolic, switching on "
"symbolic Gouy phase accumulation."
)
symbolic = True
if symbolic:
return cytools.compute_symbolic_acc_gouy(t, q_in, direction, deg)
return cytools.compute_numeric_acc_gouy(t, complex(q_in), direction, deg)
### Arbitrary beam propagations ###
[docs]def propagate_beam(
from_node=None,
to_node=None,
via_node=None,
path=None,
q_in=None,
direction="x",
symbolic=False,
simplify=False,
solution_name=None,
reverse_propagate=False,
**kwargs,
):
"""Propagates a beam through a specified path, returning dictionaries of the beam
parameter at each node and component.
This method returns a :class:`.PropagationSolution` instance.
See :ref:`propagating_beams` for details and examples on using this function.
By setting the argument `symbolic` to true, this method will return symbolic
representations of the beam parameters, ABCD matrices and accumulated Gouy phases.
Specific symbols can be kept by passing a list of symbol names to `symbolic`
instead of a True or False flag. Any symbol names not provided will use their
current evaluated value. The `simplify` flag when True will try to apply
symbolic simplification to the beam propagation. For long propagations through
many components using many symbols, this will be slow and might be faster
not using any simplification.
The argument `q_in` can be used to specify an arbitrary input beam parameter
to be used at the starting node of the propagation. If not given then this
will be determined from a call to :meth:`.Model.beam_trace`. Arguments to this
beam trace call can be passed via the `kwargs` of this method.
Parameters
----------
from_node : :class:`.OpticalNode`
Node to trace from.
to_node : :class:`.OpticalNode`
Node to trace to.
via_node : :class:`.OpticalNode`, optional
Optional node to trace via.
path : :class:`.OpticalPath`, optional
A pre-generated path to use.
q_in : :class:`.BeamParam`, complex, optional
Beam parameter to use at starting node. If not specified then
this will be determined from a beam trace. Note that, if specified,
this can also be a symbolic beam parameter.
direction : str, optional; default: "x"
Plane of computation (can be 'x', 'y' or `None`).
symbolic : bool, tuple(Parameters), optional; default: False
If False a numerical ABCD propagation is computed. If True, a symbolic
ABCD propagation is calculated instead. A tuple of parameters can also
be provided, in this case these parameters will be kept symbolic
simplify : bool, optional
When True, symbolic simplication will be attempted. When using many
symbols and large propagation paths this will significantly increase
computational time.
reverse_propagate : bool, optional
When True, the beam will be propagated in the reverse of the optical path
found. This allows beams to be traced backwards through components like
directional beamsplitters.
Returns
-------
ps : :class:`.PropagationSolution`
A solution object for the propagation.
"""
if reverse_propagate:
path = _make_path(
OpticalNode.get_opposite_direction(to_node),
OpticalNode.get_opposite_direction(from_node),
via_node.opposite if via_node is not None else None,
path,
)
t = TraceTree.from_path([n.opposite for n in path.nodes[::-1]])
else:
path = _make_path(from_node, to_node, via_node, path)
t = TraceTree.from_path(path.nodes)
if t is None:
raise ValueError("Cannot propagate beam from a node to itself!")
q_in = _make_input_q(q_in, t.node, direction, **kwargs)
if q_in.wavelength != t.node._model.lambda0:
warn(
f"In propagate_beam:\n"
f" Wavelength of input beam parameter ({q_in.wavelength} m) not equal "
f"to wavelength of model associated with path ({t.node._model.lambda0} m)."
)
if q_in.symbolic and not symbolic:
LOGGER.info(
"In propagate_beam:\n"
" Specified q_in argument is symbolic, switching on "
"symbolic propagation."
)
symbolic = True
node_info, comp_info = handle_symbolic(
t,
q_in,
direction,
symbolic=symbolic,
simplify=simplify,
sym_func=cytools.propagate_beam_symbolic,
num_func=cytools.propagate_beam_numeric,
)
if solution_name is None:
fn = path.nodes[0]
tn = path.nodes[-1]
solution_name = f"Propagation_{fn.full_name}_{tn.full_name}_{direction}"
if symbolic:
solution_name += "_sym"
return PropagationSolution(solution_name, node_info, comp_info, symbolic)
[docs]def propagate_beam_astig(
from_node=None,
to_node=None,
via_node=None,
path=None,
qx_in=None,
qy_in=None,
symbolic=False,
solution_name=None,
reverse_propagate=False,
**kwargs,
):
"""Propagates the beam through a specified path over both the tangential and
sagittal planes.
Internally this calls :func:`~finesse.tracing.tools.propagate_beam` twice - for both
the tangential and sagittal planes - and returns a solution object which stores the
returns of these as properties.
Parameters
----------
from_node : :class:`.Node`
Node to trace from.
to_node : :class:`.Node`
Node to trace to.
via_node : :class:`.Node`, optional
Optional node to trace via.
path : :class:`.OpticalPath`, optional
A pre-generated path to use.
qx_in : :class:`.BeamParam`, complex, optional
Beam parameter, in the tangential plane, to use at starting node. If not
specified then this will be determined from a beam trace. Note that,
if specified, this can also be a symbolic beam parameter.
qy_in : :class:`.BeamParam`, complex, optional
Beam parameter, in the sagittal plane, to use at starting node. If not
specified then this will be determined from a beam trace. Note that,
if specified, this can also be a symbolic beam parameter.
symbolic : bool, optional; default: False
Flag determining whether to return a symbolic representation.
Returns
-------
astig_sol : :class:`.AstigmaticPropagationSolution`
A solution object consisting of the propagation solutions for both planes
and methods for accessing the per-plane beam parameters and overlaps.
"""
ps_x = propagate_beam(
from_node,
to_node,
via_node,
q_in=qx_in,
direction="x",
symbolic=symbolic,
reverse_propagate=reverse_propagate,
**kwargs,
)
ps_y = propagate_beam(
from_node,
to_node,
via_node,
q_in=qy_in,
direction="y",
symbolic=symbolic,
reverse_propagate=reverse_propagate,
**kwargs,
)
if solution_name is None:
fn = ps_x.start_node
tn = ps_x.end_node
solution_name = f"AstigProp_{fn.full_name}_{tn.full_name}"
if symbolic:
solution_name += "_sym"
return AstigmaticPropagationSolution(solution_name, ps_x, ps_y)
[docs]def handle_symbolic(
*args,
symbolic: tuple | bool,
simplify: bool,
sym_func: Callable,
num_func: Callable,
):
"""Handles logic for the 'symbolic' argument for :func:`propagate_beam` and
:func:`compute_abcd`.
Parameters
----------
symbolic : tuple | bool
If True or a tuple, returns a symbolic equation. If tuple, only returns symbolic
equation with symbols in the tuple. Else returns numeric.
simplify : bool
Wether to simplify the symbolics
sym_func : Callable
Function to call for symbolic solution
num_func : Callable
Function to call for numeric solution
Returns
-------
Any
Symbolic or numeric solution
Raises
------
ValueError
When 'symbolic' is not a tuple or a boolean
"""
if not isinstance(symbolic, (tuple, bool)):
raise ValueError(f"{symbolic} must be Tuple or Boolean")
if isinstance(symbolic, tuple):
_symbolic = symbolic
elif symbolic:
_symbolic = None # use all symbols
else:
return num_func(*args)
return sym_func(*args, simplify, _symbolic)
### Computing mode mismatches ###
[docs]def compute_cavity_mismatches(model, cav1=None, cav2=None):
"""Computes the mismatch parameter (see :meth:`.BeamParam.mismatch` for the
equation) between cavities of the model.
If either / both of `cav1`, `cav2` are not specified then these will be set to
all the cavities of the model. This means that the default behaviour of this method
(specifying no args) is to compute mismatches between each cavity in the model.
If either of each cavity in a coupling is unstable then the mismatch values
between these will be given as ``np.nan``.
Parameters
----------
cav1 : :class:`.Cavity`, str, optional; default: None
A single cavity object (or its name). Defaults to None such that
all cavities are used.
cav2 : :class:`.Cavity`, str, optional; default: None
A single cavity object (or its name). Defaults to None such that
all cavities are used.
Returns
-------
mmx : float or dict
If both `cav1` and `cav2` were specified then this will be a single number
giving the mismatch between these cavities in the tangential plane.
Otherwise, mmx is a dictionary of ``(c1, c2): mm_x`` mappings, where `c1`
and `c2` are the cavity names and `mm_x` is the mismatch between any two
cavities in the tangential plane.
mmy : float or dict
If both `cav1` and `cav2` were specified then this will be a single number
giving the mismatch between these cavities in the sagittal plane.
Otherwise, mmy is a dictionary of ``(c1, c2): mm_y`` mappings, where `c1`
and `c2` are the cavity names and `mm_y` is the mismatch between any two
cavities in the sagittal plane.
"""
if cav1 is None:
cavs_outer = model.cavities
else:
if isinstance(cav1, str):
_c1 = model.elements.get(cav1)
else:
_c1 = cav1
if not isinstance(_c1, Cavity):
raise ValueError(f"Invalid argument type/name for cav1: {cav1}")
cavs_outer = [_c1]
if cav2 is None:
cavs_inner = model.cavities
else:
if isinstance(cav2, str):
_c2 = model.elements.get(cav2)
else:
_c2 = cav2
if not isinstance(_c2, Cavity):
raise ValueError(f"Invalid argument type/name for cav2: {cav2}")
cavs_inner = [_c2]
mm_x = {}
mm_y = {}
for c1 in cavs_outer:
if not c1.is_stable:
for c2 in cavs_inner:
mm_x[(c1.name, c2.name)] = mm_x[(c2.name, c1.name)] = np.nan
mm_y[(c1.name, c2.name)] = mm_y[(c2.name, c1.name)] = np.nan
else:
trace = model.beam_trace(store=False, enable_only=c1)
for c2 in cavs_inner:
if not c2.is_stable:
mm_x[(c1.name, c2.name)] = mm_x[(c2.name, c1.name)] = np.nan
mm_y[(c1.name, c2.name)] = mm_y[(c2.name, c1.name)] = np.nan
elif c1 is c2:
mm_x[(c1.name, c2.name)] = mm_x[(c2.name, c1.name)] = 0
mm_y[(c1.name, c2.name)] = mm_y[(c2.name, c1.name)] = 0
else:
q1x, q1y = trace[c2.source]
q2x, q2y = c2.qx, c2.qy
mmx = BeamParam.mismatch(q1x, q2x)
mmy = BeamParam.mismatch(q1y, q2y)
mm_x[(c1.name, c2.name)] = mm_x[(c2.name, c1.name)] = mmx
mm_y[(c1.name, c2.name)] = mm_y[(c2.name, c1.name)] = mmy
if cav1 is not None and cav2 is not None:
return list(mm_x.values())[0], list(mm_y.values())[0]
return mm_x, mm_y
### Convenience functions ###
def _make_path(from_node, to_node, via_node, path):
if path is not None:
if from_node is not None or to_node is not None or via_node is not None:
raise ValueError(
"Cannot specify both path and any of " "(from_node, to_node, via_node)."
)
if not isinstance(path, OpticalPath):
raise TypeError(
"Expected path to be of type OpticalPath (i.e. return "
"value of a Model.path call), but got an object of "
f"type: {type(path)}"
)
else:
if from_node is None or to_node is None:
raise ValueError("One of: path OR (from_node, to_node) must be specified.")
model = from_node._model
if to_node._model is not model:
raise ValueError(
f"{from_node.full_name} and {to_node.full_name} are from "
"different models."
)
path = model.path(from_node, to_node, via_node=via_node)
return path
def _make_input_q(q_in, node, direction, **kwargs):
model = node._model
if q_in is None:
trace = model.beam_trace(store=False, **kwargs)
qx_in, qy_in = trace[node]
if direction == "x":
q_in = qx_in
else:
q_in = qy_in
else:
model._update_symbolic_abcds()
if not isinstance(q_in, BeamParam):
q_in = BeamParam(
q=q_in,
wavelength=model.lambda0,
nr=refractive_index(node),
)
return q_in