# flake8: noqa
"""Finesse legacy (Finesse 2) kat-script parser."""
from collections import OrderedDict
import logging
import re
from sly import Lexer, Parser
from finesse.exceptions import FinesseException
from .. import Model, analysis, components, constants, detectors, symbols
from ..frequency import Frequency
from ..utilities import opened_file
from ..env import warn
LOGGER = logging.getLogger(__name__)
# Mapping of some alternate legacy attribute names to finesse 3 versions
# Use a list type for 1:n mappings
attribute_map = {
"M": "mass",
"m": "mass",
"Mass": "mass",
"Rx": "Rcx",
"rx": "Rcx",
"rcx": "Rcx",
"rocx": "Rcx",
"ROCx": "Rcx",
"Ry": "Rcy",
"ry": "Rcy",
"rcy": "Rcy",
"rocy": "Rcy",
"ROCy": "Rcy",
"g": ["user_gouy_x", "user_gouy_y"],
"gx": "user_gouy_x",
"gy": "user_gouy_y",
}
[docs]def select_powerdetector(*args, **kwargs):
if len(args) + len(kwargs) <= 2:
return detectors.PowerDetector(*args, **kwargs)
elif len(args) + len(kwargs) <= 4:
new_kwargs = {}
for k, v in kwargs.items():
if k == "f1" or k == "phase1":
new_kwargs[k.strip("1")] = v
else:
new_kwargs[k] = v
return detectors.PowerDetectorDemod1(*args, **new_kwargs)
elif len(args) + len(kwargs) <= 6:
return detectors.PowerDetectorDemod2(*args, **kwargs)
[docs]def select_quantum_noisedetector(*args, **kwargs):
shot_only = kwargs.pop("shot_only")
if len(args) + len(kwargs) <= 3:
if shot_only:
return detectors.QuantumShotNoiseDetector(*args, **kwargs)
else:
return detectors.QuantumNoiseDetector(*args, **kwargs)
elif len(args) + len(kwargs) <= 5:
new_kwargs = {}
for k, v in kwargs.items():
if k == "f1" or k == "phase1":
new_kwargs[k.strip("1")] = v
else:
new_kwargs[k] = v
if shot_only:
return detectors.QuantumShotNoiseDetectorDemod1(*args, **new_kwargs)
else:
return detectors.QuantumNoiseDetectorDemod1(*args, **new_kwargs)
elif len(args) + len(kwargs) <= 7:
if shot_only:
return detectors.QuantumShotNoiseDetectorDemod2(*args, **kwargs)
else:
return detectors.QuantumNoiseDetectorDemod2(*args, **kwargs)
[docs]def get_const(consts, value):
if type(value) is str and value.strip("+-") in consts:
ret = consts[value.strip("+-")]
if value.startswith("-"):
ret *= -1
else:
ret = value
return ret
[docs]def get_model_element(model, element):
try:
return model.elements[element]
except KeyError:
raise KeyError(f"Element '{element}' not in model.")
[docs]class KatParser:
"""Kat file lexer, parser and builder."""
[docs] def parse(self, text, model=None, **kwargs):
"""Parse kat code into a model.
Parameters
----------
text : str
String containing the kat code to be parsed.
model : :class:`.Model`, optional
Model object to add components to. If not specified, a new model will be
created.
ignored_blocks : list, optional
A list of names of ``FTBLOCK`` sections in the kat code to leave out of the
model; defaults to empty list.
Returns
-------
:class:`.Model`
The constructed model.
Raises
------
:class:`.KatParserError`
If an error occurs during parsing or building.
"""
if model is not None:
LOGGER.info(f"Parsing into existing model {model!r}.")
else:
model = Model()
return self._build(self._parse(text), model, **kwargs)
[docs] def parse_file(self, path, model=None, **kwargs):
"""Parse kat code from a file into a model.
Parameters
----------
path : str or :py:class:`io.FileIO`
The path or file object to read kat script from. If an open file object is
passed, it will be read from and left open. If a path is passed, it will be
opened, read from, then closed.
model : :class:`.Model`, optional
Model object to add components to. If not specified, a new model will be
created.
Other Parameters
----------------
**kwargs
Keyword parameters supported by :meth:`.parse`.
Raises
------
:class:`.KatParserError`
If an error occurs during parsing or building.
"""
with opened_file(path, "r") as fobj:
LOGGER.info(f"Parsing kat script from {fobj.name}")
return self.parse(fobj.read(), model=model, **kwargs)
def _parse(self, text):
"""Parses kat code into a model.
Parameters
----------
text : str
String containing the kat code to be parsed.
Returns
-------
parser : :class:`.sly.Parser`
The parser object containing the parsed blocks.
Raises
------
KatParserError
If an error occurs during parsing.
"""
lexer = _KatLEX()
parser = _KatYACC()
# As we have no way of detecting EOF and calling pop_state() from
# within the component lexer, we must ensure that all files end in a
# newline
text = f"{text}\n"
# Trim any whitespace within $$ strings
matches = re.findall(r"\$\$[^$]+\$\$", text)
for match in matches:
text = text.replace(match, re.sub(r"\s+", "", match))
tokens = lexer.tokenize(text)
parser.parse(tokens)
errors = sorted(lexer.errors + parser.errors, key=lambda tup: tup[1])
if len(errors) > 0:
raise KatParserError(errors, text)
for warning in lexer.warnings:
warn(f"{warning[1]}:{find_column(text, warning[2])}: {warning[0]}")
return parser
def _build(self, parser, model=None, ignored_blocks=None):
"""Constructs a new model or appends to an existing model using the parsed kat
code.
Parameters
----------
parser : :class:`.sly.Parser`
The parser object containing the parsed blocks.
model : :class:`.Model`
Model object to update.
ignored_blocks : list, optional
A list of names of ``FTBLOCK`` sections in the kat code to leave out of the model;
defaults to empty list.
Returns
-------
:class:`.Model`
The constructed model.
"""
blocks = parser.blocks
if ignored_blocks is None:
ignored_blocks = []
def parse_parameter(param):
if type(param) is not str or "$" not in param:
return param
local = dict(map(lambda f: (f.name, f), model.frequencies))
local.update(model.alternate_name_map)
local = {**local, **model.elements}
if param.endswith("$"):
p = re.sub("([a-zA-Z_][a-zA-Z0-9_:.]*)", r"\1.ref", param)
else:
p = param
return eval(f"{p}".replace("$", ""), local)
LOGGER.info("Building model")
component_constructors = {
"lasers": components.Laser,
"squeezers": components.Squeezer,
"mirrors": components.Mirror,
"beamsplitters": components.Beamsplitter,
"directional_beamsplitters": components.DirectionalBeamsplitter,
"isolators": components.Isolator,
"modulators": components.Modulator,
"lenses": components.Lens,
}
detector_constructors = {
"amplitude_detectors": detectors.AmplitudeDetector,
"beam_detectors": (detectors.CCDPixel, detectors.FieldPixel),
"beam_property_detectors": detectors.BeamPropertyDetector,
"power_detectors": select_powerdetector,
"quantum_noise_detectors": select_quantum_noisedetector,
}
model = model or Model()
node_names = ["node", "node1", "node2", "node3", "node4"]
nodes = {}
consts = {}
ignored_blocks = set(ignored_blocks)
if ignored_blocks:
ignored_block_list = ", ".join(ignored_blocks)
LOGGER.debug(f"Ignoring blocks {ignored_block_list}.")
for key in ignored_blocks:
# TODO: make KatParserError behave like the kat3 parser's, i.e. it can be instantiated
# with a single message and not just a list of errors, then if a KeyError is thrown
# here, rethrow a KatParserError with a message about the ignored block not existing.
blocks.pop(key)
# First pass, just grab constants
for block, d in blocks.items():
for k, v in d["constants"].items():
consts[k] = v
def apply_constants(comp):
if isinstance(comp, list):
for k, v in enumerate(comp):
if isinstance(v, dict) or isinstance(v, list):
apply_constants(v)
else:
comp[k] = get_const(consts, v)
if isinstance(comp, dict):
for k, v in comp.items():
if isinstance(v, dict) or isinstance(v, list):
apply_constants(v)
else:
comp[k] = get_const(consts, v)
# And apply any constant renaming
for block, d in blocks.items():
for name, comp_type in d.items():
if name == "constants":
continue
apply_constants(comp_type)
# Variables
for block, d in blocks.items():
for k, v in d["variables"].items():
model.add(components.Variable(k, v))
# Next grab source frequencies
for block, d in blocks.items():
for f in d["frequencies"]:
model.add_frequency(
Frequency(f["name"], model, symbols.Constant(f["f"]))
)
# Next construct all frequencies & normal components,
# and get node names
for block, d in blocks.items():
for name, constructor in component_constructors.items():
for comp in d[name]:
args = []
ns = []
lineno = None
for k, v in comp.items():
if k == "lineno":
lineno = v
elif k in node_names:
ns.append(v)
else:
args.append(v)
el = constructor(*args)
if lineno:
el._legacy_script_line_number = lineno
model.add(el)
for i, n in enumerate(ns):
if n in nodes and n != "dump":
raise ValueError(
f"In block '{block}': {args[0]}: "
f"Node '{n}' already assigned to "
f"'{nodes[n][0]}'."
)
else:
nodes[n] = (args[0], ns.index(n) + 1)
# get the output node object corresponding to
# this node and tag it
_ni = getattr(
getattr(
get_model_element(model, comp["name"]), f"p{i + 1}"
),
"o",
)
try:
model.tag_node(_ni, n)
except:
pass
# Connect all of the components up with spaces
for block, d in blocks.items():
for space in d["spaces"]:
try:
comp1 = nodes[space["node1"]]
node1 = getattr(get_model_element(model, comp1[0]), f"p{comp1[1]}")
except KeyError:
name = space["name"] + "_" + space["node1"]
comp1 = components.Nothing(name)
model.add(comp1)
node1 = comp1.p1
nodes[space["node1"]] = (name, 1)
try:
comp2 = nodes[space["node2"]]
node2 = getattr(get_model_element(model, comp2[0]), f"p{comp2[1]}")
except KeyError:
name = space["name"] + "_" + space["node2"]
comp2 = components.Nothing(name)
model.add(comp2)
node2 = comp2.p1
nodes[space["node2"]] = (name, 1)
kwargs = {}
lineno = None
for k, v in space.items():
if k == "lineno":
lineno = v
elif k not in node_names:
kwargs[k] = v
el = components.Space(**kwargs, portA=node1, portB=node2)
model.add(el)
if lineno:
el._legacy_script_line_number = lineno
# Create fsigs
for block, d in blocks.items():
for fsig in d["fsigs"]:
if model.fsig.f.value is None:
model.fsig.f = parse_parameter(fsig["f"])
model.alternate_name_map["fs"] = model.fsig.f.ref
model.alternate_name_map["mfs"] = model.fsig.f.ref
elif model.fsig.f.value != parse_parameter(fsig["f"]):
raise ValueError("Cannot have more than one signal frequency.")
# fsig command is specifying an input not just a frequency
if len(fsig.keys()) == 2:
# Need to make some dummy object here to reference
# the model fsig value legacy issues of dealing with
# a single parameter that can have multiple names
model.fsig.f = fsig["f"]
model.alternate_name_map[fsig["name"]] = model.fsig
else:
comp = get_model_element(model, fsig["component"])
mod_type = fsig["mod_type"]
scaling = 1
if isinstance(comp, components.Mirror) or isinstance(
comp, components.Beamsplitter
):
node = comp.mech.z
if mod_type is None or mod_type == "phase":
scaling = model.lambda0 / (2 * constants.PI)
else:
raise ValueError(
f"Unsupported signal type '{mod_type}' at a mirror."
)
else:
if mod_type == "amp":
node = comp.amp.i
elif mod_type == "phase":
node = comp.phs.i
elif mod_type == "freq":
node = comp.frq.i
elif mod_type is None:
if isinstance(comp, components.Laser):
node = comp.frq.i
elif isinstance(comp, components.Space):
node = comp.h.i
else:
node = comp.phs.i
else:
raise ValueError(f"Unknown signal type '{mod_type}'.")
signal = components.SignalGenerator(
fsig["name"], node, fsig["amp"] * scaling, fsig["phase"]
)
model.add(signal)
# Create detectors
for block, d in blocks.items():
for name, constructor in detector_constructors.items():
for det in d[name]:
args = {}
lineno = None
for k, v in det.items():
if k == "lineno":
lineno = v
elif k not in node_names:
if isinstance(v, list):
args[k] = [parse_parameter(x) for x in v]
else:
args[k] = parse_parameter(v)
else:
direction = "o"
if v.endswith("*"):
v = v.strip("*")
direction = "i"
comp = get_model_element(model, nodes[v][0])
if isinstance(comp, components.Nothing) or isinstance(
comp, components.Laser
):
if direction == "i":
direction = "o"
else:
direction = "i"
node = f"p{nodes[v][1]}.{direction}"
n = comp
for attr in node.split("."):
n = getattr(n, attr)
args["node"] = n
if name == "beam_detectors":
if args["f"] is None: # CCDPixel
del args["f"]
constructor = constructor[0]
else: # FieldPixel
constructor = constructor[1]
comp = constructor(**args)
comp._legacy_script_line_number = lineno
model.add(comp)
# Gouy detector
for block, d in blocks.items():
for det in d["gouy"]:
args = [det["name"]]
for space in det["space_list"]:
args.append(get_model_element(model, space))
el = detectors.Gouy(*args, direction=det["direction"])
el._legacy_script_line_number = det["lineno"]
model.add(el)
# Motion detector
for block, d in blocks.items():
for det in d["motion_detectors"]:
name = det["name"]
node = getattr(
get_model_element(model, det["component"]).mech, det["motion"]
)
el = detectors.MotionDetector(name, node)
el._legacy_script_line_number = det["lineno"]
model.add(el)
# Apply attributes
for block, d in blocks.items():
for k, v in d["attributes"].items():
component = get_model_element(model, k)
for attr, val in v:
if attr in attribute_map:
attr = attribute_map[attr]
if attr == "mass":
name = component.name + "_free_mass"
model.add(
components.mechanical.FreeMass(name, component.mech, val)
)
else:
# accept 1:n mappings
if type(attr) is list:
for a in attr:
setattr(component, a, val)
else:
setattr(component, attr, val)
cavities = []
# Create cavities
for block, d in blocks.items():
for cav in d["cavities"]:
try:
comp1 = nodes[cav["node1"]]
except KeyError:
raise KeyError(f"Node '{cav['node11']}' not in model.")
try:
comp2 = nodes[cav["node2"]]
except KeyError:
raise KeyError(f"Node '{cav['node2']}' not in model.")
node1 = getattr(get_model_element(model, comp1[0]), f"p{comp1[1]}")
node2 = getattr(get_model_element(model, comp2[0]), f"p{comp2[1]}")
if comp1[0] != comp2[0]:
el = components.Cavity(cav["name"], node1.o, node2.i)
else:
el = components.Cavity(cav["name"], node1.o)
el._legacy_script_line_number = cav["lineno"]
model.add(el)
cavities.append(el)
gausses = []
# Gauss commands
for block, d in blocks.items():
for gauss in d["gauss"]:
try:
comp = nodes[gauss["node"]]
except KeyError:
raise KeyError(f"Node '{gauss['node']}' not in model.")
node = getattr(get_model_element(model, comp[0]), f"p{comp[1]}").o
if gauss["component"] != comp[0]:
el = get_model_element(model, gauss["component"])
if isinstance(el, components.Space):
if node.port.space != el:
raise KeyError(
f"Invalid node '{gauss['node']}' for component '{gauss['component']}'."
)
# If a space was specified, flip the direction around
node = node.port.i
else:
raise KeyError(
f"Invalid node '{gauss['node']}' for component '{gauss['component']}'."
)
gauss_kwargs = {}
if "qy_re" in gauss:
gauss_kwargs["qx"] = gauss["qx_re"] + 1j * gauss["qx_im"]
gauss_kwargs["qy"] = gauss["qy_re"] + 1j * gauss["qy_im"]
elif "qx_re" in gauss:
gauss_kwargs["q"] = gauss["qx_re"] + 1j * gauss["qx_im"]
elif "w0y" in gauss:
gauss_kwargs["w0x"] = gauss["w0x"]
gauss_kwargs["zx"] = gauss["zx"]
gauss_kwargs["w0y"] = gauss["w0y"]
gauss_kwargs["zy"] = gauss["zy"]
else:
gauss_kwargs["w0"] = gauss["w0x"]
gauss_kwargs["z"] = gauss["zx"]
gauss_obj = components.Gauss(gauss["name"], node, **gauss_kwargs)
model.add(gauss_obj)
gausses.append(gauss_obj)
# Want to keep Finesse 2 behaviour as much as possible in legacy parsing, so
# here we set the priority values of the dependencies based on the order in
# which the cavities and gausses were parsed to be consistent with Finesse 2
Ndeps = len(cavities) + len(gausses)
for i, dep in enumerate(cavities + gausses):
dep.priority = Ndeps - i
# Max TEM
for block, d in blocks.items():
maxtem = parse_parameter(d["maxtem"])
if maxtem == "off" or maxtem is None:
model.switch_off_homs()
else:
model.modes(maxtem=maxtem)
# Phase command
for block, d in blocks.items():
model.phase_level = parse_parameter(d["phase"])
# Lambda command
for block, d in blocks.items():
if d["lambda"] is not None:
model.lambda0 = d["lambda"]
# Retrace command
for block, d in blocks.items():
if d["retrace"] is None or d["retrace"] == "":
continue
elif d["retrace"] == "off":
model.sim_trace_config["retrace"] = False
else:
warn(f"Unknown retrace argument '{d['retrace']}'; ignoring")
# Startnode command
for block, d in blocks.items():
if d["startnode"] is not None:
comp = nodes[d["startnode"]]
node = getattr(get_model_element(model, comp[0]), f"p{comp[1]}").o
associated_gauss = model.gausses.get(node)
if associated_gauss is None:
LOGGER.error(
"startnode %s does not correspond to any Gauss command.",
node.full_name,
)
# Get the current maximum priority (guaranteed to be first dependency)
max_priority = model.trace_order[0].priority
# Set the startnode Gauss as the new highest priority
associated_gauss.priority = max_priority + 1
# Input TEMs
for block, d in blocks.items():
for tem in d["tems"]:
args = dict(tem)
component = get_model_element(model, args.pop("component"))
component.tem(**args)
# Photo-detector types
for block, d in blocks.items():
for pdtype in d["pdtypes"]:
args = dict(pdtype)
detector = get_model_element(model, args.pop("detector"))
if not (
isinstance(detector, detectors.PowerDetector)
or isinstance(detector, detectors.PowerDetectorDemod1)
or isinstance(detector, detectors.PowerDetectorDemod2)
):
raise ValueError("Cannot apply pdtype to a non pd detector.")
# update the detector
detector.pdtype = args["type"][0] + "split"
# Detector masks
for block, d in blocks.items():
for mask in d["masks"]:
args = dict(mask)
detector = get_model_element(model, args.pop("detector"))
detector.add_to_mask(**args)
analyses = []
# Xaxis
camera_replacement = None
for block, d in blocks.items():
xaxis = d["xaxis"]
x2axis = d["x2axis"]
x3axis = d["x3axis"]
if xaxis is None:
continue
xaxis["steps"] = int(xaxis["steps"])
if xaxis["component"] in model.alternate_name_map:
comp = model.alternate_name_map[xaxis["component"]]
else:
comp = get_model_element(model, f"{xaxis['component']}")
# store this before converting with attribute getter below
# as we want to use this for camera axis sweep checking
xax_param = xaxis["parameter"]
# phase1 & f1 for a pd1 have had the 1 removed, so rename them here
if isinstance(comp, detectors.PowerDetectorDemod1) and xax_param[-1] == "1":
xax_param = xax_param[:-1]
xaxis["parameter"] = getattr(comp, xax_param)
# cannot scan axes of Pixel, this is now done via ScanLine or Image type
# cameras so we begin the process of transforming to one of these here
if not (
isinstance(comp, detectors.camera.Pixel) and xax_param in ("x", "y")
):
model.alternate_name_map["x1"] = xaxis["parameter"].ref
model.alternate_name_map["mx1"] = -xaxis["parameter"].ref
del xaxis["component"]
if xaxis["starred"]:
xaxis["offset"] = xaxis["parameter"].value
else:
xaxis["offset"] = 0
if isinstance(comp, detectors.camera.Pixel) and xax_param in ("x", "y"):
LOGGER.info(
"Found an xaxis scan over the %s parameter of the "
"Pixel detector %s --> Replacing this detector and axis "
"sweep with a ScanLine detector.",
xax_param,
comp.name,
)
node = comp.node
lim = [xaxis["min"], xaxis["max"]]
npts = xaxis["steps"] + 1
direction = xax_param
if hasattr(comp, "f"):
f = comp.f.value
else:
f = None
# remove the Pixel detectors and add a ScanLine detector
# in replacement of the axis sweep
model.remove(comp)
if f is None:
camera_replacement = detectors.CCDScanLine(
comp.name,
node,
npts,
xlim=lim if direction == "x" else None,
ylim=lim if direction == "y" else None,
)
else:
camera_replacement = detectors.FieldScanLine(
comp.name,
node,
npts,
xlim=lim if direction == "x" else None,
ylim=lim if direction == "y" else None,
f=f,
)
model.add(camera_replacement)
if x2axis is None:
if camera_replacement is None:
analyses.append(
analysis.actions.Xaxis(
xaxis["parameter"],
xaxis["scale"],
xaxis["min"],
xaxis["max"],
xaxis["steps"],
relative=xaxis["offset"],
)
)
else:
x2axis["steps"] = int(x2axis["steps"])
if x2axis["component"] in model.alternate_name_map:
comp = model.alternate_name_map[x2axis["component"]]
else:
comp = get_model_element(model, f"{x2axis['component']}")
if isinstance(comp, detectors.camera.ScanLine):
if xax_param in ("x", "y") and x2axis["parameter"] in ("x", "y"):
LOGGER.info(
"Found an x2axis scan over the %s parameter of the previous "
"Pixel detector %s --> Replacing the ScanLine detector "
"added previously with a ComplexCamera.",
xax_param,
comp.name,
)
node = comp.node
x1ax_dir = xax_param
x2ax_dir = x2axis["parameter"]
if x1ax_dir == x2ax_dir:
raise ValueError(
"Cannot scan the same axis of the beam analyser twice."
)
if x1ax_dir == "x":
xlim = [xaxis["min"], xaxis["max"]]
ylim = [x2axis["min"], x2axis["max"]]
else:
xlim = [x2axis["min"], x2axis["max"]]
ylim = [xaxis["min"], xaxis["max"]]
npts = xaxis["steps"] + 1
if hasattr(comp, "f"):
f = comp.f.value
else:
f = None
# remove the ScanLine detector and add a ComplexCamera
# in replacement of axes sweeps
model.remove(comp)
if f is None:
camera_replacement = detectors.CCD(
comp.name,
node,
xlim,
ylim,
npts,
)
else:
camera_replacement = detectors.FieldCamera(
comp.name, node, xlim, ylim, npts, f
)
model.add(camera_replacement)
else:
x2axis["parameter"] = getattr(comp, f"{x2axis['parameter']}")
model.alternate_name_map["x2"] = x2axis["parameter"].ref
model.alternate_name_map["mx2"] = -x2axis["parameter"].ref
del x2axis["component"]
if x2axis["starred"]:
x2axis["offset"] = x2axis["parameter"].value
else:
x2axis["offset"] = 0
if x3axis is None:
if camera_replacement is None:
analyses.append(
analysis.actions.X2axis(
xaxis["parameter"],
xaxis["scale"],
xaxis["min"],
xaxis["max"],
xaxis["steps"],
x2axis["parameter"],
x2axis["scale"],
x2axis["min"],
x2axis["max"],
x2axis["steps"],
)
)
elif isinstance(camera_replacement, detectors.camera.ScanLine):
analyses.append(
analysis.actions.Xaxis(
x2axis["parameter"],
x2axis["scale"],
x2axis["min"],
x2axis["max"],
x2axis["steps"],
relative=x2axis["offset"],
)
)
else:
x3axis["steps"] = int(x3axis["steps"])
if x3axis["component"] in model.alternate_name_map:
comp = model.alternate_name_map[x3axis["component"]]
else:
comp = get_model_element(model, f"{x3axis['component']}")
x3axis["parameter"] = getattr(comp, f"{x3axis['parameter']}")
model.alternate_name_map["x3"] = x3axis["parameter"].ref
model.alternate_name_map["mx3"] = -x3axis["parameter"].ref
del x3axis["component"]
if x3axis["starred"]:
x3axis["offset"] = x3axis["parameter"].value
else:
x3axis["offset"] = 0
if camera_replacement is None:
analyses.append(
analysis.actions.X3axis(
xaxis["parameter"],
xaxis["scale"],
xaxis["min"],
xaxis["max"],
xaxis["steps"],
x2axis["parameter"],
x2axis["scale"],
x2axis["min"],
x2axis["max"],
x2axis["steps"],
x3axis["parameter"],
x3axis["scale"],
x3axis["min"],
x3axis["max"],
x3axis["steps"],
)
)
elif isinstance(camera_replacement, detectors.camera.ScanLine):
analyses.append(
analysis.actions.X2axis(
x2axis["parameter"],
x2axis["scale"],
x2axis["min"],
x2axis["max"],
x2axis["steps"],
x3axis["parameter"],
x3axis["scale"],
x3axis["min"],
x3axis["max"],
x3axis["steps"],
)
)
elif isinstance(camera_replacement, detectors.camera.Image):
analyses.append(
analysis.actions.Xaxis(
x3axis["parameter"],
x3axis["scale"],
x3axis["min"],
x3axis["max"],
x3axis["steps"],
relative=x3axis["offset"],
)
)
# Yaxis
for block, d in blocks.items():
for yaxis in d["yaxis"]:
if model.yaxis is not None:
raise ValueError("Cannot have more than one yaxis command.")
model.yaxis = yaxis
# Sets
for block, d in blocks.items():
for name, cmd in d["sets"].items():
comp = get_model_element(model, cmd["component"])
if isinstance(comp, detectors.Detector):
raise ValueError(
"Finesse 3 does not support using 'set' with the output of a detector. If "
"you are using this in combination with a 'lock' command, please switch "
"to the new syntax and use the 'lock' command as defined there."
)
model.alternate_name_map[name] = getattr(
get_model_element(model, cmd["component"]), cmd["parameter"]
)
# Funcs
for block, d in blocks.items():
for name, func in d["functions"].items():
model.alternate_name_map[name] = parse_parameter(func)
# Puts
scanning_im_ax = None
for block, d in blocks.items():
for put in d["puts"]:
value = parse_parameter(put["variable"])
if put["add"]:
component = get_model_element(model, put["component"])
param = getattr(component, put["parameter"])
setattr(component, put["parameter"], param + value)
else:
component = get_model_element(model, put["component"])
if isinstance(component, components.SignalGenerator):
setattr(model.fsig, put["parameter"], value)
elif isinstance(component, detectors.camera.Pixel):
if isinstance(
camera_replacement, detectors.camera.ScanLine
) and put["parameter"] in ("x", "y"):
model.remove(component)
if camera_replacement.direction == "x":
npts = camera_replacement.x.shape[0]
else:
npts = camera_replacement.y.shape[0]
if hasattr(component, "f"):
model.add(
detectors.FieldScanLine(
component.name,
component.node,
npts,
camera_replacement.x,
camera_replacement.y,
camera_replacement.xlim,
camera_replacement.ylim,
component.f.value,
)
)
else:
model.add(
detectors.CCDScanLine(
component.name,
component.node,
npts,
camera_replacement.x,
camera_replacement.y,
camera_replacement.xlim,
camera_replacement.ylim,
)
)
elif isinstance(camera_replacement, detectors.camera.Image):
if scanning_im_ax is None:
scanning_im_ax = put["parameter"]
continue
elif (
scanning_im_ax == "x"
and put["parameter"] == "y"
or scanning_im_ax == "y"
and put["parameter"] == "x"
):
model.remove(component)
if hasattr(component, "f"):
model.add(
detectors.FieldCamera(
component.name,
component.node,
camera_replacement.x,
camera_replacement.y,
camera_replacement.x.shape[0],
component.f.value,
)
)
else:
model.add(
detectors.CCD(
component.name,
component.node,
camera_replacement.x,
camera_replacement.y,
camera_replacement.x.shape[0],
)
)
else:
setattr(component, put["parameter"], value)
else:
setattr(component, put["parameter"], value)
scales = {}
for block, d in blocks.items():
for scale in d["scales"]:
v = scale["value"]
if isinstance(v, str):
import numpy as np
s = v.lower()
if s == "deg":
scale["value"] = 180 / np.pi
elif s == "rad":
scale["value"] = np.pi / 180
elif s == "meter":
scale["value"] = 2 * np.pi / model.lambda0
else:
LOGGER.error(
f"Scale type '{v}' not recognised, not performing scaling."
)
scale["value"] = 1
comp = scale["component"]
if comp is not None:
if comp in scales:
scales[comp] *= scale["value"]
else:
scales[comp] = scale["value"]
else:
for det in model.detectors:
if det.name in scales:
scales[det.name] *= scale["value"]
else:
scales[det.name] = scale["value"]
if len(scales) > 0:
analyses[0] = analysis.actions.Serial(
analyses[0], analysis.actions.Scale("Scale", scales)
)
if len(analyses) > 1:
raise NotImplementedError(
"Handling multiple analyses for legacy parsing not implemented yet"
)
if not analyses:
analyses.append(analysis.actions.Noxaxis())
model.analysis = analyses[0]
def sort(item):
has = hasattr(item[1], "_legacy_script_line_number")
return (not has, item[1]._legacy_script_line_number if has else None)
model.sort_elements(key=sort)
for el in model.elements.values():
if hasattr(el, "_legacy_script_line_number"):
del el._legacy_script_line_number
return model
class _KatLEX(Lexer):
"""Kat file lexer, default state."""
# Set case-insensitive flag for re in sly's Lexer class
reflags = re.IGNORECASE
tokens = {
"AMPLITUDE_DETECTOR",
"ATTRIBUTE",
"BEAM_DETECTOR",
"BEAM_PROPERTY_DETECTOR",
"BEAM_SPLITTER",
"CAVITY",
"COMMENT_START",
"CONSTANT",
"DIRECTIONAL_BEAM_SPLITTER",
"SOURCE_FREQUENCY",
"FTBLOCK_END",
"FTBLOCK_START",
"FUNCTION",
"FSIG",
"GAUSS",
"GNUPLOT_START",
"GOUY",
"ISOLATOR",
"LAMBDA",
"LASER",
"LENS",
"LOCK",
"MASK",
"MAXTEM",
"MIRROR",
"MODULATOR",
"MOTION_DETECTOR",
"NOXAXIS",
"PDTYPE",
"PHASE",
"POWER_DETECTOR",
"PUT",
"QUANTUM_NOISE_DETECTOR",
"QUANTUM_SHOT_NOISE_DETECTOR",
"RETRACE",
"SCALE",
"SET",
"SPACE",
"SQUEEZER",
"STARTNODE",
"TEM",
"VARIABLE",
"XAXIS",
"X2AXIS",
"X3AXIS",
"YAXIS",
}
@_(
"color",
"conf",
"debug",
"frequency",
"gnuterm",
"noplot",
"pause",
"printmatrix",
"pyterm",
"showiterate",
"time",
"trace",
"width",
)
def obsolete(self, t):
self.warnings.append(
(f"Command '{t.value}' is obsolete, ignoring.", self.lineno, 1)
)
line = self.text.split("\n")[self.lineno - 1]
self.index += len(line) - len(t.value)
@_(
"multi",
"pdS",
"pdN",
"hd",
"qd",
"sd",
"qhd",
"qhdS",
"qhdN",
"pgaind",
"fadd",
"map",
"knm",
"smotion",
"vacuum",
"tf",
"tf2",
"func",
"diff",
"deriv_h",
)
def not_implemented(self, t):
self.warnings.append(
(f"Command '{t.value}' not yet implemented.", self.lineno, 1)
)
line = self.text.split("\n")[self.lineno - 1]
self.index += len(line) - len(t.value)
# In order to allow components which are substrings of other components
# (e.g. 'l' and 'lens'), these should be sorted alphabetically and then in
# length order, such that no string is a substring of one that comes later
# in the list.
AMPLITUDE_DETECTOR = r"ad\s"
ATTRIBUTE = r"attr\s"
BEAM_SPLITTER = r"(beamsplitter|bs)[1-2]?\s"
BEAM_DETECTOR = r"beam\s"
BEAM_PROPERTY_DETECTOR = r"bp\s"
CAVITY = r"cav(ity)?\s"
COMMENT_START = r"/\*"
CONSTANT = r"const\s"
DIRECTIONAL_BEAM_SPLITTER = r"dbs\s"
SOURCE_FREQUENCY = r"freq\s"
FSIG = r"fsig\s"
FTBLOCK_END = r"FTend"
FTBLOCK_START = r"FTblock"
FUNCTION = r"func\s"
GAUSS = r"gauss\*?\s"
# Note: see issue #529: we cannot use (?i) flag at the beginning of patterns
# here, we now instead use global case-insensitive matching for tokens by
# setting reflags.
GNUPLOT_START = r"GNUPLOT"
GOUY = r"gouy\s"
ISOLATOR = r"(isol|diode)\s"
LAMBDA = r"lambda0?\s"
LENS = r"lens\s"
LOCK = r"lock\*?\s"
LASER = r"(laser|light|l)\s"
MASK = r"mask\s"
MAXTEM = r"maxtem\s"
MODULATOR = r"mod\s"
MIRROR = r"(mirror|m[1-2]?)\s"
NOXAXIS = r"noxaxis"
PDTYPE = r"pdtype\s"
PHASE = r"phase\s"
POWER_DETECTOR = r"pd[0-9]*\s"
PUT = r"put\*?\s"
QUANTUM_NOISE_DETECTOR = r"qnoisedS?\s"
QUANTUM_SHOT_NOISE_DETECTOR = r"qshotS?\s"
RETRACE = r"retrace"
STARTNODE = r"startnode\s"
SCALE = r"scale\s"
SET = r"set\s"
SQUEEZER = r"sq\s"
SPACE = r"(space|s)\s"
TEM = r"tem\s"
VARIABLE = r"variable\s"
X3AXIS = r"x3axis\*?\s"
X2AXIS = r"x2axis\*?\s"
XAXIS = r"xaxis\*?\s"
MOTION_DETECTOR = r"xd\s"
YAXIS = r"yaxis\s"
# Ignored patterns.
ignore = "[ \t]"
ignore_comment = "#.*"
ignore_comment2 = "%((?!FT).)*"
def __init__(self):
self.reset()
def reset(self):
self.errors = []
self.warnings = []
@_(r"\n+")
def ignore_newline(self, t):
self.lineno += t.value.count("\n")
def error(self, t):
line = t.value.split("\n")[0]
command = line.split(" ")[0]
self.errors.append(
(f"Command '{command}' unrecognised", self.lineno, self.index)
)
self.index += len(line)
return t
def eof(self, t):
print("EOF")
# Command type tokens.
def AMPLITUDE_DETECTOR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def ATTRIBUTE(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def BEAM_DETECTOR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def BEAM_PROPERTY_DETECTOR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def BEAM_SPLITTER(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def CAVITY(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def COMMENT_START(self, t):
self.push_state(_KatCommentLEX)
def CONSTANT(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def DIRECTIONAL_BEAM_SPLITTER(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def SOURCE_FREQUENCY(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def FTBLOCK_START(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def FTBLOCK_END(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def FUNCTION(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def FSIG(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def GAUSS(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def GNUPLOT_START(self, t):
self.push_state(_KatGnuplotLEX)
def GOUY(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def ISOLATOR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def LAMBDA(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def LASER(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def LENS(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def LOCK(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def MASK(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def MAXTEM(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def MIRROR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def MODULATOR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def MOTION_DETECTOR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def NOXAXIS(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def PDTYPE(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def PHASE(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def POWER_DETECTOR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def PUT(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def QUANTUM_NOISE_DETECTOR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def QUANTUM_SHOT_NOISE_DETECTOR(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def RETRACE(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def SCALE(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def SET(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def SPACE(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def SQUEEZER(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def STARTNODE(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def TEM(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def VARIABLE(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def XAXIS(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def X2AXIS(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def X3AXIS(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
def YAXIS(self, t):
self.push_state(_KatComponentLEX)
t.value = t.value.strip()
return t
class _KatCommentLEX(Lexer):
"""Kat file lexer, comment state."""
tokens = {"END"}
@_(r"\n+")
def ignore_newline(self, t):
self.lineno += t.value.count("\n")
def error(self, t):
self.index += 1
return
@_(r"\*/")
def END(self, t):
self.pop_state()
class _KatGnuplotLEX(_KatCommentLEX):
"""Kat file lexer, gnuplot state."""
tokens = {"END"}
@_("END")
def END(self, t):
self.pop_state()
class _KatComponentLEX(Lexer):
"""Kat file lexer, component state."""
tokens = {"FUNCTIONSTRING", "NUMBER", "STRING"}
# Top level tokens.
FUNCTIONSTRING = r"=[^=\n#]+"
ignore = " \t"
ignore_comment = r"\#.*"
ignore_comment2 = "%.*"
@_(r"\n+")
def ignore_newline(self, t):
self.lineno += t.value.count("\n")
self.pop_state()
@_(r"[$0-9\-+*(][a-zA-Z0-9_\-+*$().]+")
def NUM_PARAM(self, t):
if "$" not in t.value:
# This is a number
t.type = "NUMBER"
return self.NUMBER(t)
t.type = "NUMBER"
return t
# Number token including scientific notation, float,
# or +/- inf (all states). Alternatively, any string starting with $
@_(
r"[+-]?inf",
r"[+-]?(\d+\.\d*|\d*\.\d+|\d+)([eE]-?\d*\.?\d*)?([pnumkMGT])?",
r"\$[.\s]+",
)
def NUMBER(self, t):
if t.value.startswith("$"):
return t
if re.match(".*[pnumkMGT]$", t.value):
t.value = t.value.replace("p", "e-12")
t.value = t.value.replace("n", "e-9")
t.value = t.value.replace("u", "e-6")
t.value = t.value.replace("m", "e-3")
t.value = t.value.replace("k", "e3")
t.value = t.value.replace("M", "e6")
t.value = t.value.replace("G", "e9")
t.value = t.value.replace("T", "e12")
# Check for numbers ending in "e", like "1e", which Python's float cannot handle.
if t.value.endswith("e"):
t.value += "0"
if "j" in t.value:
t.value = complex(t.value)
else:
t.value = float(t.value)
if t.value.is_integer():
t.value = int(t.value)
return t
@_(r"[a-zA-Z_][a-zA-Z0-9_:+-]*\*?", "inf")
def STRING(self, t):
if t.value == "inf":
t.type = "NUMBER"
return self.NUMBER(t)
return t
def error(self, t):
line = t.value.split("\n")[0].split(" ")[0]
self.errors.append(
(f"Illegal character '{t.value[0]}'", self.lineno, self.index)
)
self.index += len(line)
return t
class _KatYACC(Parser):
"""Kat file parser."""
tokens = set.union(_KatLEX.tokens, _KatComponentLEX.tokens)
tokens.remove("COMMENT_START")
tokens.remove("GNUPLOT_START")
# Setting STRING and NUMBER to have the same precedence and be right-associative solves the
# shift/reduce conflict caused by the frequency_list rule in favour of shifting. We must then
# be careful to extract the final string, as for e.g. the powerdetector, this is actually a
# node name, not a phase.
precedence = (("right", "STRING", "NUMBER"),)
def __init__(self):
self.reset()
def reset(self):
"""Delete all parsed code, resetting the parser to a newly constructed state."""
self.noxaxis = False
self.block = None
self.blocks = OrderedDict()
self.blocks[self.block] = self._default_components()
self.errors = []
def _default_components(self):
return {
# Default simulation components.
"lasers": [],
"spaces": [],
"mirrors": [],
"beamsplitters": [],
"directional_beamsplitters": [],
"isolators": [],
"modulators": [],
"lenses": [],
"amplitude_detectors": [],
"beam_detectors": [],
"beam_property_detectors": [],
"gouy": [],
"motion_detectors": [],
"power_detectors": [],
"quantum_noise_detectors": [],
"cavities": [],
"squeezers": [],
# Non-component commands
"frequencies": [],
"constants": {},
"attributes": {},
"variables": {},
"functions": {},
"sets": {},
"fsigs": [],
"gauss": [],
"locks": [],
"puts": [],
"pdtypes": [],
"maxtem": None,
"phase": 3,
"retrace": None,
"startnode": None,
"scales": [],
"tems": [],
"masks": [],
"xaxis": None,
"x2axis": None,
"x3axis": None,
"yaxis": [],
"lambda": None,
}
@_("instruction", "statement instruction")
def statement(self, p):
pass
# List of one or more numbers
@_("NUMBER", "number_list NUMBER")
def number_list(self, p):
if len(p) == 1:
return [p.NUMBER]
else:
p.number_list.append(p.NUMBER)
return p.number_list
# List of one or more strings
@_("STRING", "string_list STRING")
def string_list(self, p):
if len(p) == 1:
return [p.STRING]
else:
p.string_list.append(p.STRING)
return p.string_list
# List of frequency-phase pairs
@_(
"NUMBER STRING",
"NUMBER NUMBER",
"frequency_list NUMBER STRING",
"frequency_list NUMBER NUMBER",
)
def frequency_list(self, p):
if len(p) == 2:
return [[p[0], p[1]]]
else:
p.frequency_list.append([p[1], p[2]])
return p.frequency_list
# List of attribute-value pairs
@_("STRING NUMBER", "attribute_list STRING NUMBER")
def attribute_list(self, p):
if len(p) == 2:
return [[p.STRING, p.NUMBER]]
else:
p.attribute_list.append([p.STRING, p.NUMBER])
return p.attribute_list
# Frequencies can either be a number or a combination of source frequencies
@_("NUMBER")
def freq_num(self, p):
return p[0]
@_("FTBLOCK_START STRING")
def instruction(self, p):
if self.block is not None:
warn(f"Already in FTblock {self.block}")
if p.STRING in self.blocks:
warn(f"Duplicate FTblock {p.STRING}")
self.block = p.STRING
self.blocks[self.block] = self._default_components()
@_("FTBLOCK_END STRING")
def instruction(self, p):
if self.block != p.STRING:
message = (
f"Invalid command 'FTend {p.STRING}': currently in "
f"FTblock '{self.block}'"
)
self.errors.append((message, p.lineno, p.index))
self.block = None
@_("SOURCE_FREQUENCY STRING NUMBER")
def instruction(self, p):
params = ["name", "f"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["frequencies"].append(dict(zip(params, values)))
@_("LASER STRING NUMBER freq_num optnum STRING")
def instruction(self, p):
# Phase not specified.
if p[4] is None:
p[4] = 0
params = ["name", "P", "f", "phase", "node"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["lasers"].append(dict(zip(params, values)))
self.blocks[block]["lasers"][-1]["lineno"] = p.lineno
@_("SQUEEZER STRING freq_num NUMBER NUMBER STRING")
def instruction(self, p):
params = ["name", "f", "db", "angle", "node"]
values = [p[i] for i in range(1, len(p))]
# Some minor rearranging required here to match the component constructor
params.insert(2, params.pop(1))
values.insert(2, values.pop(1))
block = self.block
self.blocks[block]["squeezers"].append(dict(zip(params, values)))
self.blocks[block]["squeezers"][-1]["lineno"] = p.lineno
@_("SPACE STRING NUMBER optnum STRING STRING")
def instruction(self, p):
params = ["name", "L", "nr", "node1", "node2"]
values = [p[i] for i in range(1, len(p))]
if values[2] is None:
# Index of refraction not specified.
# TODO:phil: should we be specifying 1 as the default here, or
# checking for None in the space constructor later?
values[2] = 1
block = self.block
self.blocks[block]["spaces"].append(dict(zip(params, values)))
self.blocks[block]["spaces"][-1]["lineno"] = p.lineno
@_("MIRROR STRING NUMBER NUMBER NUMBER STRING STRING")
def instruction(self, p):
params = ["name", "R", "T", "L", "phi", "node1", "node2"]
values = [p[i] for i in range(1, len(p))]
if p[0] == "m" or p[0] == "mirror":
# R / T
values.insert(3, None)
elif p[0] == "m1":
# T / Loss.
values.insert(1, None)
elif p[0] == "m2":
# R / Loss.
values.insert(2, None)
block = self.block
self.blocks[block]["mirrors"].append(dict(zip(params, values)))
self.blocks[block]["mirrors"][-1]["lineno"] = p.lineno
@_("BEAM_SPLITTER STRING NUMBER NUMBER NUMBER NUMBER STRING STRING STRING STRING")
def instruction(self, p):
params = [
"name",
"R",
"T",
"L",
"phi",
"alpha",
"node1",
"node2",
"node3",
"node4",
]
values = [p[i] for i in range(1, len(p))]
if p[0].endswith("2"):
# R / Loss.
values.insert(2, None)
elif p[0].endswith("1"):
# T / Loss.
values.insert(1, None)
else:
# R / T
values.insert(3, None)
block = self.block
self.blocks[block]["beamsplitters"].append(dict(zip(params, values)))
self.blocks[block]["beamsplitters"][-1]["lineno"] = p.lineno
@_("DIRECTIONAL_BEAM_SPLITTER STRING STRING STRING STRING STRING")
def instruction(self, p):
params = ["name", "node1", "node2", "node3", "node4"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["directional_beamsplitters"].append(
dict(zip(params, values))
)
self.blocks[block]["directional_beamsplitters"][-1]["lineno"] = p.lineno
@_("ISOLATOR STRING NUMBER STRING STRING")
def instruction(self, p):
params = ["name", "S", "node1", "node2"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["isolators"].append(dict(zip(params, values)))
self.blocks[block]["isolators"][-1]["lineno"] = p.lineno
@_(
"MODULATOR STRING freq_num NUMBER NUMBER STRING optnum STRING STRING",
"MODULATOR STRING freq_num NUMBER STRING STRING optnum STRING STRING",
)
def instruction(self, p):
params = ["name", "f", "midx", "order", "type", "phase", "node1", "node2"]
values = [p[i] for i in range(1, len(p))]
# TODO:phil: is no phase actually the same as 0 phase?
if values[5] is None:
values[5] = 0
if values[3] == "s":
values[3] = 1
params.append("positive_only")
values.append(True)
block = self.block
self.blocks[block]["modulators"].append(dict(zip(params, values)))
self.blocks[block]["modulators"][-1]["lineno"] = p.lineno
@_("LENS STRING NUMBER STRING STRING")
def instruction(self, p):
params = ["name", "f", "node1", "node2"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["lenses"].append(dict(zip(params, values)))
self.blocks[block]["lenses"][-1]["lineno"] = p.lineno
@_(
"AMPLITUDE_DETECTOR STRING NUMBER NUMBER freq_num STRING",
"AMPLITUDE_DETECTOR STRING freq_num STRING",
)
def instruction(self, p):
params = ["name", "n", "m", "f", "node"]
values = [p[i] for i in range(1, len(p))]
if len(values) == 3:
# Mode numbers not specified.
values.insert(1, None)
values.insert(2, None)
block = self.block
self.blocks[block]["amplitude_detectors"].append(dict(zip(params, values)))
self.blocks[block]["amplitude_detectors"][-1]["lineno"] = p.lineno
@_("BEAM_DETECTOR STRING optnum STRING")
def instruction(self, p):
params = ["name", "f", "node"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["beam_detectors"].append(dict(zip(params, values)))
self.blocks[block]["beam_detectors"][-1]["lineno"] = p.lineno
@_("BEAM_PROPERTY_DETECTOR STRING STRING STRING STRING")
def instruction(self, p):
params = ["name", "direction", "prop", "node"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["beam_property_detectors"].append(dict(zip(params, values)))
self.blocks[block]["beam_property_detectors"][-1]["lineno"] = p.lineno
@_("GOUY STRING STRING string_list")
def instruction(self, p):
params = ["name", "direction", "space_list"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["gouy"].append(dict(zip(params, values)))
self.blocks[block]["gouy"][-1]["lineno"] = p.lineno
@_("MOTION_DETECTOR STRING STRING STRING")
def instruction(self, p):
params = ["name", "component", "motion"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["motion_detectors"].append(dict(zip(params, values)))
self.blocks[block]["motion_detectors"][-1]["lineno"] = p.lineno
@_(
"POWER_DETECTOR STRING STRING",
"POWER_DETECTOR STRING frequency_list optstr",
"POWER_DETECTOR STRING number_list STRING",
)
def instruction(self, p):
# Parameters shared by all photodetectors.
params = ["name", "node"]
values = [p[i] for i in range(1, len(p))]
ps = ["f{}", "phase{}"]
if len(values) == 3:
try:
p.number_list
for n in range(len(values[1])):
params.insert(n + 1, ps[n % 2].format(n // 2 + 1))
values = [values[0], *values[1], values[2]]
except AttributeError:
if p.optstr is None:
# We've grabbed the node as part of the frequency list, so put it back in the
# right place
values[2] = values[1][-1].pop()
for n in range(len(values[1])):
for m in range(len(values[1][n])):
params.insert(2 * n + m + 1, ps[m].format(n + 1))
values = [
values[0],
*[param for pair in values[1] for param in pair],
values[2],
]
block = self.block
self.blocks[block]["power_detectors"].append(dict(zip(params, values)))
self.blocks[block]["power_detectors"][-1]["lineno"] = p.lineno
@_(
"QUANTUM_NOISE_DETECTOR STRING number_list STRING",
)
def instruction(self, p):
# Parameters shared by all quantum noise detectors.
params = ["name", "node"]
values = [p[i] for i in range(1, len(p))]
ps = ["f{}", "phase{}"]
# Strip last demodulation, as this should always be at the signal frequency
nf = len(values[1]) // 2 - 1
for n in range(2 * nf):
params.insert(n + 1, ps[n % 2].format(n // 2 + 1))
values = [values[0], *values[1][1 : 2 * nf + 1], values[2]]
params.insert(-1, "shot_only")
values.insert(-1, False)
params.insert(-1, "nsr")
values.insert(-1, p[0].endswith("S"))
block = self.block
self.blocks[block]["quantum_noise_detectors"].append(dict(zip(params, values)))
self.blocks[block]["quantum_noise_detectors"][-1]["lineno"] = p.lineno
@_(
"QUANTUM_SHOT_NOISE_DETECTOR STRING number_list STRING",
)
def instruction(self, p):
# Parameters shared by all quantum noise detectors.
params = ["name", "node"]
values = [p[i] for i in range(1, len(p))]
ps = ["f{}", "phase{}"]
# Strip last demodulation, as this should always be at the signal frequency
nf = len(values[1]) // 2 - 1
for n in range(2 * nf):
params.insert(n + 1, ps[n % 2].format(n // 2 + 1))
values = [values[0], *values[1][1 : 2 * nf + 1], values[2]]
params.insert(-1, "shot_only")
values.insert(-1, True)
params.insert(-1, "nsr")
values.insert(-1, p[0].endswith("S"))
block = self.block
self.blocks[block]["quantum_noise_detectors"].append(dict(zip(params, values)))
self.blocks[block]["quantum_noise_detectors"][-1]["lineno"] = p.lineno
@_("CAVITY STRING STRING STRING STRING STRING")
def instruction(self, p):
params = ["name", "component1", "node1", "component2", "node2"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["cavities"].append(dict(zip(params, values)))
self.blocks[block]["cavities"][-1]["lineno"] = p.lineno
@_("CONSTANT STRING NUMBER", "CONSTANT STRING STRING")
def instruction(self, p):
name = p[1]
val = p[2]
block = self.block
self.blocks[block]["constants"]["$" + name] = val
@_("ATTRIBUTE STRING attribute_list")
def instruction(self, p):
comp = p[1]
attrs = p[2]
block = self.block
if comp in self.blocks[block]["attributes"]:
self.blocks[block]["attributes"][comp].extend(attrs)
else:
self.blocks[block]["attributes"][comp] = attrs
@_("VARIABLE STRING NUMBER", "VARIABLE STRING STRING")
def instruction(self, p):
name = p[1]
value = p[2]
block = self.block
self.blocks[block]["variables"][name] = value
@_("FUNCTION STRING FUNCTIONSTRING")
def instruction(self, p):
name = p[1]
function_string = p[2]
# Trim the starting "=" from the function string, and any whitespace
block = self.block
self.blocks[block]["functions"][name] = function_string[1:].strip()
@_("FSIG STRING STRING optstr freq_num NUMBER optnum", "FSIG STRING NUMBER")
def instruction(self, p):
if len(p) == 3:
params = ["name", "f"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["fsigs"].append(dict(zip(params, values)))
else:
if p.optnum is None:
# Set default amplitude to 1
p[-1] = 1
params = ["name", "component", "mod_type", "f", "phase", "amp"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["fsigs"].append(dict(zip(params, values)))
@_("LOCK STRING NUMBER NUMBER NUMBER")
def instruction(self, p):
params = ["name", "variable", "gain", "accuracy", "starred"]
values = [p[i] for i in range(1, len(p))]
if p[0].endswith("*"):
values.append(True)
else:
values.append(False)
block = self.block
self.blocks[block]["locks"].append(dict(zip(params, values)))
@_(
"GAUSS STRING STRING STRING NUMBER NUMBER",
"GAUSS STRING STRING STRING NUMBER NUMBER NUMBER NUMBER",
)
def instruction(self, p):
values = [p[i] for i in range(1, len(p))]
if p[0].endswith("*"):
params = ["name", "component", "node", "qx_re", "qx_im", "qy_re", "qy_im"]
else:
params = ["name", "component", "node", "w0x", "zx", "w0y", "zy"]
block = self.block
self.blocks[block]["gauss"].append(dict(zip(params, values)))
@_("PUT STRING STRING NUMBER")
def instruction(self, p):
params = ["component", "parameter", "variable", "add"]
values = [p[i] for i in range(1, len(p))]
if p[0].endswith("*"):
values.append(True)
else:
values.append(False)
block = self.block
self.blocks[block]["puts"].append(dict(zip(params, values)))
@_("SCALE NUMBER optstr", "SCALE STRING optstr")
def instruction(self, p):
params = ["value", "component"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["scales"].append(dict(zip(params, values)))
@_("SET STRING STRING STRING")
def instruction(self, p):
name = p[1]
params = ["component", "parameter"]
values = [p[i] for i in range(2, len(p))]
block = self.block
self.blocks[block]["sets"][name] = dict(zip(params, values))
@_("PDTYPE STRING STRING")
def instruction(self, p):
params = ["detector", "type"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["pdtypes"].append(dict(zip(params, values)))
@_("MAXTEM NUMBER", "MAXTEM STRING")
def instruction(self, p):
block = self.block
self.blocks[block]["maxtem"] = p[1]
@_("PHASE NUMBER")
def instruction(self, p):
block = self.block
self.blocks[block]["phase"] = p[1]
@_("RETRACE optstr")
def instruction(self, p):
block = self.block
self.blocks[block]["retrace"] = p[1] or ""
@_("STARTNODE STRING")
def instruction(self, p):
block = self.block
self.blocks[block]["startnode"] = p[1]
@_("TEM STRING NUMBER NUMBER NUMBER NUMBER")
def instruction(self, p):
params = ["component", "n", "m", "factor", "phase"]
values = [p[i] for i in range(1, len(p))]
block = self.block
self.blocks[block]["tems"].append(dict(zip(params, values)))
@_("MASK STRING NUMBER NUMBER NUMBER")
def instruction(self, p):
params = ["detector", "modes", "factor"]
values = [p[1], (p[2], p[3]), p[4]]
block = self.block
# ignoring factor as masks in Finesse 3 just zero the given field
self.blocks[block]["masks"].append(dict(zip(params[:-1], values[:-1])))
@_("LAMBDA NUMBER")
def instruction(self, p):
block = self.block
self.blocks[block]["lambda"] = p.NUMBER
@_("NOXAXIS")
def instruction(self, p):
self.noxaxis = True
@_("XAXIS STRING STRING STRING NUMBER NUMBER NUMBER")
def instruction(self, p):
params = ["component", "parameter", "scale", "min", "max", "steps", "starred"]
values = [p[i] for i in range(1, len(p))]
if p[0].endswith("*"):
values.append(True)
else:
values.append(False)
block = self.block
self.blocks[block]["xaxis"] = dict(zip(params, values))
@_("X2AXIS STRING STRING STRING NUMBER NUMBER NUMBER")
def instruction(self, p):
params = ["component", "parameter", "scale", "min", "max", "steps", "starred"]
values = [p[i] for i in range(1, len(p))]
if p[0].endswith("*"):
values.append(True)
else:
values.append(False)
block = self.block
self.blocks[block]["x2axis"] = dict(zip(params, values))
@_("X3AXIS STRING STRING STRING NUMBER NUMBER NUMBER")
def instruction(self, p):
params = ["component", "parameter", "scale", "min", "max", "steps", "starred"]
values = [p[i] for i in range(1, len(p))]
if p[0].endswith("*"):
values.append(True)
else:
values.append(False)
block = self.block
self.blocks[block]["x3axis"] = dict(zip(params, values))
# TODO: placing the optstr before the STRING like in Finesse 2 causes a
# shift/reduce conflict - can we solve this?
@_("YAXIS STRING optstr")
def instruction(self, p):
params = ["scale", "axes"]
values = [p[i] for i in range(1, len(p))]
if values[1] is None:
values.insert(0, "lin")
block = self.block
self.blocks[block]["yaxis"].append(dict(zip(params, values)))
def error(self, p):
if p is None:
msg = "Unexpected end of file"
self.errors.append((msg, None, None))
return
elif p.type == "ERROR":
return
msg = f"got unexpected token {p.value} of type {p.type}"
self.errors.append((msg, p.lineno, p.index))
@_("")
def empty(self, p):
pass
@_("STRING")
def optstr(self, p):
return p.STRING
@_("empty")
def optstr(self, p):
pass
@_("NUMBER")
def optnum(self, p):
return p.NUMBER
@_("empty")
def optnum(self, p):
pass
[docs]class KatParserError(FinesseException): # __NODOC__
"""Kat file parser error."""
def __init__(self, errors, text, **kwargs):
message = "\n"
for error in errors:
lineno = error[1]
idx = error[2]
if lineno is None:
# There is no lineno, as this was an end-of-file error,
# so assume error was on last non-empty lin
line = re.findall(r"[^\s]", text)[-1]
pos = len(text) - 1
else:
line = text.split("\n")[lineno - 1]
pos = find_column(text, idx)
expected = ""
for pattern, exp in self.expected.items():
if exp is not None and re.match(pattern, line) is not None:
expected = f", expected '{exp}'"
break
message += f"{lineno}:{pos}: " + error[0] + expected + "\n"
message += line + "\n"
message += " " * (pos - 1) + "^\n"
super().__init__(message.rstrip("\n"), **kwargs)
expected = {
r"ad\s": "ad name [n m] f node[*]",
r"attr\s": "attr component parameter value",
r"beam\s": "beam name [f] node[*]",
r"bp\s": "bp name x/y parameter node",
r"bs2\s": "bs2 name R L phi alpha node1 node2 node3 node4",
r"bs1\s": "bs1 name T L phi alpha node1 node2 node3 node4",
r"bs\s": "bs name R T phi alpha node1 node2 node3 node4",
r"cav\s": "cav name component1 node component2 node",
r"const\s": "const name value",
r"dbs\s": "dbs name node1 node2 node3 node4",
r"freq\s": None,
r"FTblock": "FTblock name",
r"FTend": "FTend name",
r"func\s": "func name = function-string",
r"fsig\s": ("fsig name component [type] f phase [amp]", "fsig name f"),
r"gauss*\s": "gauss* name component node q [qy]",
r"gauss\s": "gauss name component node w0 z [wy0 zy]",
r"gouy\s": "gouy name x/y space-list",
r"isol\s": "isol name S [Loss] node1 node2 [node3]",
r"lambda\s": "lambda wavelength",
r"lock\s": "lock name function/set gain accuracy [offset]",
r"lens\s": "lens name f node1 node2",
r"l\s": "l name I f [phase] node",
r"maxtem\s": "maxtem order",
r"mod\s": "mod name f midx order am/pm/yaw/pitch node1 node2",
r"m2\s": "m2 name R L phi node1 node2",
r"m1\s": "m1 name T L phi node1 node2",
r"m\s": "m name R T phi node1 node2",
r"noxaxis": None,
r"pd\s": "pd[n] name [f1 [phase1 [f2 [phase2 [...] ] ] ] ] node[*]",
r"phase\s": "phase 0-7",
r"put\s": "put component parameter function/set/axis",
r"qnoised\s": "qnoised name num_demods f1 phase1 [f2 phase2 [...]] node[*]",
r"qshot\s": "qshot name num_demods f1 phase1 [f2 phase2 [...]] node[*]",
r"retrace": "retrace [off|force]",
r"s\s": "s name L [n] node1 node2",
r"tem\s": "tem input n m factor phase",
r"variable\s": "variable name value",
r"xaxis\s": "xaxis component parameter lin/log min max steps",
r"x2axis\s": "x2axis component parameter lin/log min max steps",
r"yaxis\s": "yaxis [lin/log] abs:deg/db:deg/re:im/abs/db/deg",
}
[docs]def find_column(text, index): # __NODOC__
last_cr = text.rfind("\n", 0, index)
if last_cr < 0:
last_cr = 0
column = index - last_cr
return column