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

WIP commit

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