Commit 75c846e5 authored by Sean Leavey's avatar Sean Leavey

WIP commit

parent e727a77a
......@@ -316,11 +316,23 @@ class Model:
def elements(self):
Dictionary of all the model elements with the keys as their names
"""Map of model element names to :class:`.ModelElement`."""
return self.__elements.copy()
def param_paths(self):
"""Map of the model elements' parameters' paths (e.g. "l1.P") to parameters.
Paths as :py:class:`str` to :class:`.Parameter` objects.
return {
param.full_name: param
for element in self.elements.values()
for param in element.parameters
def network(self):
"""The directed graph object containing the optical
......@@ -24,7 +24,6 @@ class ParameterRef(Symbol):
def __init__(self, param):
self.__param = weakref.ref(param)
self.__name = f"{}.{}"
def parameter(self):
......@@ -32,7 +31,7 @@ class ParameterRef(Symbol):
def name(self):
return self.__name
return f"{}.{}"
def owner(self):
......@@ -106,7 +105,7 @@ cdef class Parameter:
self.__rebuild_flag = flag
self.__is_tunable = False
self.__validator = fn = ModelElement._validators[self.__component_type][]
self.__validator = ModelElement._validators[self.__component_type][]
self.__post_setter = ModelElement._post_setters[self.__component_type][]
self.__resolving = False
......@@ -276,7 +275,7 @@ cdef class Parameter:,
if self.locked:
raise ParameterLocked()
......@@ -11,7 +11,8 @@ from .base import BaseKatParser
from .spec import ParsedStatement, KatSpec
from .lexer import KatLexer
from .parser import KatYACC, BaseClosure
from .exceptions import KatParserError, KatReferenceError, DirectiveNotFoundError
from .exceptions import KatParserError, DirectiveNotFoundError
from ..element import _create_model_parameter
from ..utilities import ngettext, option_list
LOGGER = logging.getLogger(__name__)
......@@ -86,13 +87,14 @@ class KatParser(BaseKatParser):
if errors:
raise KatParserError.with_syntax_errors(errors, self.text)
# from networkx.drawing.nx_agraph import view_pygraphviz
# view_pygraphviz(parser.ast, prog="dot")
return self._build(parser, model)
def _build(self, parser, model):
"""Constructs a new model or appends to an existing model using the parsed kat code."""
from networkx.drawing.nx_agraph import view_pygraphviz
view_pygraphviz(parser.ast, prog="dot")
# Get the objects to build in topological order, i.e. the order that ensures dependent
# objects are parsed ahead of dependee objects. There are some exceptions such as cavities
# which have an indirect requirement to be parsed last.
......@@ -113,6 +115,9 @@ class KatParser(BaseKatParser):
# Elements moved to last in the build queue.
punted = []
# Map of parameter paths (e.g. "l1.P") to :class:`.Parameter` objects.
param_paths = model.param_paths()
while node_names:
node_name = node_names.popleft()
......@@ -120,18 +125,13 @@ class KatParser(BaseKatParser):
# Get the statement or parameter stored as node data for the node name.
item = parser.ast.nodes[node_name]["item"]
except KeyError:
# The item refers to another item that's not in the parsed text.
# Check if it's already in the model (e.g. if the user is parsing into an existing
# model).
except AttributeError as e2:
raise KatReferenceError(node_name, model) from e2
# The node name is a reference from a parsed statement that exists in the model
# already, so we don't need to build it.
# Assume this is an object that exists already in the model and not something that
# needs built.
LOGGER.debug(f"Skipping {node_name} (item not found in AST)")
LOGGER.debug(f"Building {node_name} ({item})")
if isinstance(item, ParsedStatement):
if item.adapter.build_last and item not in punted:
......@@ -144,22 +144,32 @@ class KatParser(BaseKatParser):
# Do what the instruction is supposed to do: add the component to the model, set the
# model field, etc.
# print("APPLY STATEMENT", node_name, item)
# print("APPLY PARAM", node_name, repr(item))
# This is a parameter. Get its corresponding statement.
# This is a parameter to a statement. Get its corresponding statement.
item_statement = parser.ast.nodes[node_name]["statement"]
if isinstance(item, BaseClosure):
# Resolve. Any dependencies should already have been built due to the
# topological ordering.
raise NotImplementedError
# item = create_parameter()
# Resolve the closure. Any dependencies should already have been added to the
# param_paths memo because we're iterating in topological order.
value = item.resolve(param_paths)
# raise KatReferenceError(node_name, model) from e
value = item
arg_name = item_statement.get_arg_name(node_name)
if item_statement.is_model_parameter(arg_name):
# Create the :class:`.Parameter`.
value = _create_model_parameter(
item_statement.adapter.ptype, arg_name, value
assert node_name not in param_paths, "Path collision"
param_paths[node_name] = value
# Update the corresponding statement value.
item_statement.setarg(node_name, item)
item_statement.set_arg_value(node_name, value)
if model.analysis is None:
# No analysis was set by the script. Default to noxaxis.
......@@ -10,16 +10,8 @@ from ..utilities import option_list
class KatReferenceError(FinesseException):
"""Reference to a non-existent kat script element."""
def __init__(self, element, model, kat_field=None):
msg = f"'{element}' does not exist in the model"
if kat_field is None:
msg += f" (trying to access '{kat_field}' in {model!r})"
self.element = element
self.kat_field = kat_field
def __init__(self, path, model):
super().__init__(f"'{path}' does not exist in {model}")
class KatParserError(FinesseException):
......@@ -5,6 +5,7 @@
import abc
import operator
import logging
import re
from sly import Parser
from networkx import DiGraph
......@@ -12,7 +13,7 @@ from networkx import DiGraph
from .spec import KatSpec, ParsedStatement
from .lexer import KatLexer, KatParameterLexer
from .exceptions import KatParserError, KatReferenceError
from ..utilities import option_list
from ..utilities import option_list, pairwise
LOGGER = logging.getLogger(__name__)
......@@ -58,23 +59,23 @@ class StringClosure(BaseClosure, metaclass=abc.ABCMeta):
string : str
path : str
A string taken from kat-script that should be used to find the Finesse object, such as
m1.p2.o in the case of a :class:`.ModelParameterClosure`.
"m1.p2.o" in the case of a :class:`.ModelParameterClosure`.
def __init__(self, string):
# Copy the string explicitly to avoid reference issues.
self.string = str(string)
def __init__(self, path):
# Copy the path explicitly to avoid reference issues.
self.path = str(path)
def dependencies(self):
"""The string representations of the elements or parameters that this closure depends on."""
return [self.string]
return [self.path]
def pieces(self):
return self.string.split(".")
return self.path.split(".")
def __repr__(self):
# Used for tests, such as `tests/unit/script/parser/syntax/`.
......@@ -85,17 +86,19 @@ class ModelParameterClosure(StringClosure):
"""Closure for strings that should resolve to Finesse model parameters."""
def resolve(self, memo):
raise NotImplementedError
parameter = memo[self.path]
return parameter.value
class ModelParameterReferenceClosure(StringClosure):
"""Closure for strings that should resolve to Finesse model parameter references."""
def __init__(self, string):
def __init__(self, path):
def resolve(self, memo):
raise NotImplementedError
parameter = memo[self.path]
return parameter.ref
def __repr__(self):
# Used for tests, such as `tests/unit/script/parser/syntax/`.
......@@ -123,12 +126,21 @@ class FunctionClosure(BaseClosure):
def dependencies(self):
return self._dependencies
def resolve(self, memo):
return self.function(memo)
def __repr__(self):
return f"{self.function.__name__}({', '.join([repr(d) for d in self.dependencies])})"
class KatYACC(Parser):
# Silence shift/reduce conflict warning.
log = logging.getLogger()
# Regular expression to match references to ports and nodes.
_port_node_regex = re.compile(r"^(\w+).(p\d+)(.[io])?$")
# Grab the tokens this parser uses from the various tokenisers.
tokens = set.union(
KatLexer.tokens, KatParameterLexer.tokens, ("MULTILINESTART", "MULTILINEEND")
......@@ -207,21 +219,36 @@ class KatYACC(Parser):
# Add the statement as an AST node.
self.ast.add_node(statement.node_name, item=statement)
# Add additional edges on the AST representing each arg's dependencies.
for name, parameter in statement.flattened_arguments():
# Add additional edges on the AST representing each statement's dependencies (arguments,
# ports, etc.).
for name, parameter in statement.dependencies():
# Globally unique node name.
node_name = f"{statement.node_name}.{name}"
assert node_name not in self.ast
# Add the parameter as an AST node, and draw an edge to the owning statement so it gets
# built after.
self.ast.add_node(node_name, item=parameter, statement=statement)
self.ast.add_edge(node_name, statement.node_name)
# Add edges to the parameter's dependencies, to ensure they get built first.
for dependency in getattr(parameter, "dependencies", []):
# Direction is important for later topological sorting.
self.ast.add_edge(dependency, node_name)
dependencies = parameter.dependencies
except AttributeError:
# The parameter is some terminal like a float and doesn't have any dependencies.
# Add edges to the parameter's model parameter dependencies, to ensure they get
# built first.
for dependency in dependencies:
self.ast.add_edge(dependency, node_name)
# FIXME: it would be better to avoid regex here and somehow register ports and
# nodes before their constructors so we know ahead of time what to expect, then
# add these ports and nodes as AST nodes earlier in this loop.
port_node = self._port_node_regex.match(dependency)
if port_node:
# This is a port or node, so we need to draw an edge to the element.
self.ast.add_edge(, dependency)
return statement
......@@ -371,15 +398,17 @@ class KatYACC(Parser):
ret = operation(lhs, rhs)
elif not lhs_is_closure:
ret = FunctionClosure(
lambda model: operation(lhs, rhs(model)), dependencies=rhs.dependencies
lambda memo: operation(lhs, rhs.resolve(memo)),
elif not rhs_is_closure:
ret = FunctionClosure(
lambda model: operation(lhs(model), rhs), dependencies=lhs.dependencies
lambda memo: operation(lhs.resolve(memo), rhs),
ret = FunctionClosure(
lambda model: operation(lhs(model), rhs(model)),
lambda memo: operation(lhs.resolve(memo), rhs.resolve(memo)),
dependencies=lhs.dependencies + rhs.dependencies,
......@@ -159,15 +159,26 @@ class ParsedStatement:
# Store only the arguments dict.
self.arguments = bound_args.arguments
def flattened_arguments(self):
"""Bound arguments as with :attr:`.arguments` but with nested arguments flattened.
def is_model_parameter(self, argument):
return argument in self.adapter.ptype._param_dict[self.adapter.ptype]
except AttributeError:
# ptype is probably not a ModelElement.
return False
def dependencies(self):
"""Mapping of the dependency names and objects for this statement.
Dependencies for a given statement are its corresponding Python type's bound constructor
arguments as with :attr:`.arguments`, but with nested arguments flattened (used in the case
of nested analyses, for instance).
Nested keys have the parent key prepended in the form `parent_key.child_key`.
key : str
A Python constructor signature parameter.
A dependent path.
value : object
The corresponding value.
......@@ -183,13 +194,43 @@ class ParsedStatement:
return walk(self.arguments)
def getarg(self, path):
"""Get an argument value by string in the form `component.param.subparam`."""
def get_arg_name(self, path):
"""Get argument name for the specified path.
The path should have a single "." and should represent a parameter of this statement's
path : :py:class:`str`
The argument path, e.g. "l1.P".
The argument name, e.g. "P".
pieces = path.split(".")
if len(pieces) != 2:
raise ValueError(
f"'{path}' does not appear to represent an element parameter."
if pieces[-1] not in self._getarg_container(pieces):
raise ValueError(
f"'{pieces[-1]}' is not a parameter in the element represented by "
return pieces[-1]
def get_arg_value(self, path):
pieces = path.split(".")
container = self._getarg_container(pieces)
return container[pieces[-1]]
def setarg(self, path, value):
def set_arg_value(self, path, value):
"""Set an argument value by string in the form `component.param.subparam`."""
pieces = path.split(".")
container = self._getarg_container(pieces)
......@@ -205,7 +246,7 @@ class ParsedStatement:
if len(fields) > 1:
# The path contains a sub-path.
return self.arguments[fields[0]].getarg(".".join(fields))
return self.arguments[fields[0]].get_arg_value(".".join(fields))
# The remaining path is the argument key.
return self.arguments
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment