Store model parameters in the model graph
It's kind of weird that model parameters are stored in Mirror
etc. and not directly in the model graph. Doing so would have a few benefits as far as I can see. I came to this idea upon realising that a lot of the complexity in the build step in the parser and Model.add
is the translation between different graphs that fundamentally represent the same thing: an interferometer model. In the case of the parser build step, it's translating the parsed AST into ModelElement
objects, and in the case of Model.add
it's translating from ModelElement
into the model's NetworkX graph. It seems to me like it would simplify parsing a great deal if everything about the model was represented in one graph, but it also seems to me to have other benefits beyond parsing:
Advantages
References and symbols
If model parameters were part of the model graph, references between parameters would be pretty trivial to implement. Each model element like Mirror
would be nodes, and edges would connect these to their model parameters. Where model parameters reference others, there would be edges connecting them.
Expressions would be interesting. We could either represent every atomic part of the expression as a binary tree within the graph, so "a+b-c" would be
m1.R-->ADD-->a
\->SUBTRACT-->b
\->c
(ADD
and SUBTRACT
would be nodes in the model graph - and they'd need to be unique, so we'd probably rather have ADD1
, ADD2
, etc.)
or we could retain Symbol
objects and store these as their own nodes with edges drawn to the dependent parameters.
Parsing / unparsing
The serialisation/unserialisation of a model graph to/from kat script would be simplified, since the model graph would essentially be a complete description of the model. The parser could just add model graph nodes for components and parameters it parses, then draw edges between them as it sees them - no special parsing order necessarily required, and self-referencing parameters (!39 (closed)) would be automatically supported since everything would just be nodes in the same graph and not separate objects that would need to exist by that point. A validation step would be required at the end to ensure consistency (no dangling references, etc.).
Unparsing would be trivial, since it would just be a case of iterating over the model element nodes and writing lines of script. There would be a root Model
node with edges to properties like startnode
. Properties like startnode
that reference other parts of the graph could also use edges.
Graph merging
Components not attached to models, such as ones created with the Python API like m1 = Mirror("m1", R=0.5, T=0.5)
would implement their own graph with edges to their model parameters. In fact, all ModelElements
would probably have either their own graph (when unattached) or a view into the model graph (when attached). Then when added to a model, the component's graph would be merged into the main one, and the component's own graph would be updated with a subgraph view into the model's graph.
Ports and nodes (in the Finesse sense) could also be contained in the component graph and merged into the main one, allowing them to be referenced by other parts of the model once added to it (e.g. startnode
). Beam parameters can maybe stay as attributes of (Finesse) nodes and not be (graph) nodes, or, if useful for tracing, also be nodes.
This pattern would be interesting though:
m1 = Mirror("m1", R=0.5, T=0.5)
m2 = Mirror("m2", R=m1.R.ref, T=m1.T.ref)
model = Model()
model.add(m1)
model.add(m2)
Here, references are made between components not added to a model. I guess this could be handled by having m1.T.ref
etc. return a weakref. Then when added to a model, these weakrefs could be changed to edges.
Tracing
I am not sure how the trace forest stuff works, but it might not be necessary any more to store a separate data structure (the trace forest), since all of the beam parameters etc. would be accessible from the model graph and could be quickly traversed.
Visualisation
Plotting the model graph would essentially plot everything about the model. The only extra bits to add would be normal (info) parameters (like a Modulator
's order
) which could be displayed as attributes of the component node (text within the component node). And with model parameters in the main graph, you could imagine hooking up a GUI to let you manipulate the model graph "live", like Jonathan is working on.
Sub-models
I expect that future support for sub-models (to allow e.g. thick mirrors to be represented by one logical "component") would not be too tricky to implement. These could just be their own partitions of the same graph, with their own hierarchy of components, model parameters, etc., descending from another root Model
node.
Graph pruning
I also expect having everything about the model in the one graph would make it easier to identify branches that can be optimised.
Disadvantages
Speed (can probably be fixed)
The simulations need to access model parameter objects and change their values quickly, so they are implemented in Cython. If these are instead stored as part of the model graph, we'd instead have to go via NetworkX data structures which, while fast, involve dictionary lookups instead of direct memory access, so there'd probably be a speed penalty.
I expect this can be mitigated by copying across model parameters into the simulation workspace during building, but it would need lots of new code. Validators might need to be registered somewhere other than model elements - possibly also in the model graph.
Non-exhaustive list of changes
-
Model
would probably best descend fromNetworkX.DiGraph
-
ModelElement
would contain aNetworkX.DiGraph
, but not itself be one, since depending on whether it was attached or not it would either have aDiGraph
or a view into anotherDiGraph
-
Parameter
objects would probably no longer need to exist, or would be rewritten to just be a proxy (using a bunch of weakrefs, probably) for a model graph's node - Any property of
Model
that needs to be represented in kat script, likestartnode
,modes
, the frequency list, etc. would need to be a node in the model graph so dependencies (like forstartnode
) could be drawn, and so the unparser would know to dump it without needing special logic - The parser and generator would need extensive changes, but these would be simplifying changes, probably resulting in a lot of code removal and complexity reduction
- Surely many more...