import finesse
from finesse.graph import OperatorGraph
from finesse.components import Connector
from finesse.symbols import simplification, Constant, Variable
from finesse.parameter import ParameterRef
import numpy as np
from numbers import Number
from typing import Iterable
from functools import reduce
[docs]def make_optical_operator_graph(model: finesse.model.Model) -> OperatorGraph:
"""Create an optical operator graph based on the given model.
Parameters
----------
model : :class:`finesse.model.Model`
The model object containing the optical network.
Returns
-------
tuple
A tuple containing the following elements:
- node_index : dict
A dictionary mapping nodes to their corresponding indices.
- index_name : dict
A dictionary mapping indices to their corresponding nodes.
- elements : set
A set of unique elements in the optical network.
- graph :class:`finesse.graph.OperatorGraph`
An operator graph representing the optical network.
"""
network = model.optical_network
N_nodes = len(network.nodes)
node_index = {node: i for i, node in enumerate(network.nodes)}
index_name = {i: node for i, node in enumerate(network.nodes)}
graph = OperatorGraph(N_nodes)
elements = set()
for a, b in network.edges:
data = model.optical_network[a][b]
elements.add(data["owner"]())
graph.add_edge(
data["owner"]().name + "." + data["name"],
node_index[a],
node_index[b],
)
return node_index, index_name, elements, graph
[docs]def get_all_optical_equations(elements: Iterable[Connector]) -> dict:
"""Get all optical equations from a list of elements.
Parameters
----------
elements : Iterable[Connector]
A list of Connector objects representing the elements.
Returns
-------
dict
A dictionary containing all the optical equations, where the keys are the equations' names
and the values are the equations themselves.
"""
optical_equations = {}
for element in elements:
optical_equations.update(element.optical_equations())
return optical_equations
[docs]def expression_tuple_to_symbolic(expr, optical_equations):
"""Converts an expression tuple to a symbolic expression using the provided optical
equations.
Parameters
----------
expr : tuple
The expression tuple to be converted.
optical_equations : dict
A dictionary containing the optical equations.
Returns
-------
finesse.symbols.Symbol
The simplified symbolic expression.
Notes
-----
This function recursively converts an expression tuple into a symbolic expression using the provided optical equations.
The expression tuple can contain operators such as '+' and '*', and operands that are either symbols or values.
The optical equations dictionary maps symbols to their corresponding symbolic expressions.
Examples
--------
>>> from finesse.symbols import Variable
>>> optical_equations = {'a': Variable('a'), 'b': Variable('b')}
>>> expr = ('+', ('*', 'a', 2), ('*', 'b', 3))
>>> expression_tuple_to_symbolic(expr, optical_equations)
2*a + 3*b
"""
def simplify(expr):
if isinstance(expr, tuple):
operator, *operands = expr
if operator == "+":
return reduce(np.add, map(simplify, operands))
elif operator == "*":
return reduce(np.multiply, map(simplify, operands))
elif len(expr) == 1:
return optical_equations[expr[0]]
else:
raise Exception(f"Unexpected expr {expr}")
else:
return optical_equations[expr]
with simplification(True):
y = simplify(expr)
if isinstance(y, Number):
return Constant(y)
else:
return y
[docs]def make_symbolic_optical_operators(
graph: OperatorGraph,
optical_equations: dict,
include_edges: Iterable[tuple[int, int]] = None,
) -> dict:
"""Calculate symbolic optical operators for a given graph and optical equations.
Parameters
----------
graph : OperatorGraph
The graph representing the optical system.
optical_equations : dict
A dictionary mapping operator names to their corresponding optical equations.
include_edges : Iterable[tuple[int, int]], optional
The edges for which to calculate the operators. If not provided, all edges in the graph will be used.
Returns
-------
dict
A dictionary mapping edge tuples to their corresponding symbolic operators.
"""
if include_edges is None:
include_edges = tuple(graph.edges())
operators = {}
for a, b in include_edges:
operators[(a, b)] = 1
expr = graph.get_edge_operator_expression(a, b)
operators[(a, b)] = expression_tuple_to_symbolic(expr, optical_equations)
return operators
[docs]def evaluate_non_changing_symbols(equations: dict, substitutions: dict = None):
"""Evaluate the non-changing symbols in the given equations.
Parameters
----------
equations : dict
A dictionary containing the equations to be evaluated. The keys represent
the edges, and the values represent the expressions.
Returns
-------
dict
A dictionary containing the results of evaluating the non-changing symbols.
The keys represent the edges, and the values represent the evaluated
expressions.
"""
result = {}
for edge, expr in equations.items():
result[edge] = expr.expand_symbols().eval(
keep_changing_symbols=True, subs=substitutions
)
if hasattr(result[edge], "expand"):
result[edge] = result[edge].expand()
if hasattr(result[edge], "collect"):
result[edge] = result[edge].collect()
if isinstance(result[edge], Number):
result[edge] = Constant(result[edge])
return result
[docs]def get_cavities(graph: OperatorGraph):
"""Get the cavities (self loops) and their coupling status from an OperatorGraph.
Parameters
----------
graph : OperatorGraph
The OperatorGraph representing the system.
Returns
-------
cavities : tuple
A tuple of node indices which have self loops in the graph.
coupled : tuple
A tuple indicating the whether a self loop is coupled to another or not
"""
import networkx as nx
cycles = tuple(nx.simple_cycles(graph.to_networkx()))
cavities = tuple(c[0] for c in cycles if len(c) == 1)
couplings = tuple(c for c in cycles if len(c) >= 2)
coupled = tuple(any(c in cc for cc in couplings) for c in cavities)
return cavities, coupled
[docs]class ModelOperatorPicture:
"""A picture of the current model state in terms of operators applied in a reduced
graph. The graph consists of each optical node and the edges are operators
describing how optical fields propagate between nodes. Any operators that are zero
will be removed from the graph.
Attributes
----------
node_index : dict
A dictionary mapping node names to their indices.
index_name : dict
A dictionary mapping node indices to their names.
elements : list
A list of optical elements in the model.
graph : finesse.model.Graph
A reduced graph representation of the optical model.
N_reductions : int
The number of reductions performed on the graph.
model_constants : dict
A dictionary of model constants.
optical_equations : dict
A dictionary of all optical equations in the model.
evald_optical_equations : dict
A dictionary of evaluated optical equations keeping any changing symbols.
operators : dict
A dictionary of symbolic optical operators for each edge in `graph`
non_numeric_symbols : list
A list of non-numeric symbols in the model.
changing_symbols : tuple
A tuple of symbols that are changing in the model.
"""
[docs] def __init__(self, model: finesse.model.Model, evaluate=True, reduce=True):
self.model = model
(
self.node_index,
self.index_name,
self.elements,
self.graph,
) = make_optical_operator_graph(model)
self.model_constants = {
"_f0_": model._settings.f0,
}
self.optical_equations = get_all_optical_equations(self.elements)
if evaluate:
self.evald_optical_equations = evaluate_non_changing_symbols(
self.optical_equations, self.model_constants
)
else:
self.evald_optical_equations = self.optical_equations
# Find all operators that are definitely zero and
self.zeroed_edges = []
for name, expr in self.evald_optical_equations.items():
if expr == 0:
el, conn = name.split(".")
A, B = self.model.get_element(el)._registered_connections[conn]
Aidx = self.node_index[A]
Bidx = self.node_index[B]
self.zeroed_edges.append((Aidx, Bidx))
self.graph.remove_edge(Aidx, Bidx)
# Now with a bunch of zero edges removed, we can reduce the graph
self.N_reductions = self.graph.reduce() if reduce else 0
# Then compute the final reduce graph operators
self.operators = make_symbolic_optical_operators(
self.graph, self.evald_optical_equations
)
# Check if we have any zero operators, and if so, remove them
for key in tuple(self.operators.keys()):
if self.operators[key] == 0:
self.graph.remove_edge(*key)
del self.operators[key]
self.non_numeric_symbols = set()
for expr in self.evald_optical_equations.values():
self.non_numeric_symbols.update(
expr.all(
lambda a: isinstance(a, (Variable, ParameterRef))
or (isinstance(a, Constant) and a.is_named)
)
)
# Stop arbitrary set ordering
self.non_numeric_symbols = list(self.non_numeric_symbols)
self.non_numeric_symbols.sort(key=lambda s: s.name)
self.non_numeric_symbols = tuple(self.non_numeric_symbols)
self.changing_symbols = tuple(
s for s in self.non_numeric_symbols if s.is_changing
)
[docs] def solve(
self, node_index: int or str, source_nodes: Iterable[int or str]
) -> finesse.symbols.Symbol:
"""Computes a symbolic solutions for a given node within the graph. This does
not solve for the actual values of the symbols, linear operatores will be left
as is. No actual linear operation inversions will take place.
Parameters
----------
node_index : int|str
The index of the node to solve for, or string name of node, see. self.node_index.
source_nodes : Iterable[int or str]
The indices of source nodes indices or string names that should be included
Returns
-------
:class:`finesse.symbols.Symbol`
The solution for the specified node
Examples
--------
Plot network but shade out sink nodes:
>>> import finesse
>>> from finesse.simulations.graph.tools import ModelOperatorPicture
>>> model = finesse.script.parse('''
... l l1
... bs bs
... m itmx
... m itmy
... link(l1, 1, bs.p1)
... link(bs.p2, 1, itmy)
... link(bs.p3, 1, itmx)
... ''')
>>> op = ModelOperatorPicture(model, True,True)
>>> op.graph.plot(alpha_nodes=op.graph.sink_nodes());
Or just ignore sink nodes all together:
>>> op.graph.plot(ignore_nodes=[1, 2, 3]);
"""
if isinstance(node_index, str):
node_index = self.node_index[node_index]
source_nodes = [
(n if isinstance(n, int) else self.node_index[n]) for n in source_nodes
]
def _solve(node_index, source_nodes):
if self.graph.in_degree(node_index) == 0:
# Source node solutions are easy, it's either 1 to
# include it or 0 to exclude it
if node_index in source_nodes:
return Variable(f"E_{{{node_index}}}")
else:
return Constant(0)
# This is the self loop rule for graph reduction: any self loop is replaced
# by 1/(1-self_loop_gain) to any incoming edge that is not a self loop
if self.graph.has_self_loop(node_index):
self_loop_op = self.operators[(node_index, node_index)]
CLG = 1 / (1 - self_loop_op)
else:
CLG = 1
term = 0
# loop through all incoming edges for this node and sum up the results
# This is recursive as any of the incoming nodes that are not source nodes
# will also have edges that need to be summed up
for edge in self.graph.input_edges(node_index):
if edge[0] != edge[1]: # not a self-loop
term += CLG * self.operators[edge] * _solve(edge[0], source_nodes)
return term
with finesse.symbols.simplification(True):
# Always run with simplification enabled otherwise it will be a mess
return _solve(node_index, source_nodes)