Commit 9bedcab9 authored by Daniel Brown's avatar Daniel Brown

Merge branch 'feature/cli' into 'master'

Feature/cli

See merge request finesse/pykat!5
parents 13b21f6e 720a5e05
name: pykat_test
name: pykatcli
channels:
- gwoptics
- defaults
......@@ -11,3 +11,4 @@ dependencies:
- pandas
- matplotlib
- finesse=2.2
- ipython
\ No newline at end of file
......@@ -20,6 +20,9 @@ requirements:
- flask >=0.10.1
- finesse >=2.2
- h5py
- graphviz
- python-graphviz
- click
build:
- python
- setuptools
......
"""Finesse command line interface
Sean Leavey
<sean.leavey@ligo.org>
"""
import sys
import os
import numpy as np
import click
from . import __version__
from .finesse import kat as katparser
@click.command(help="Python interface and tools for FINESSE")
@click.argument("file", type=click.File())
@click.option("--simulate/--no-simulate", is_flag=True, default=True,
help="Simulate FILE in Finesse. Can be set to --no-simulate if for example "
"you only want to display the node graph for the model specified in FILE.")
@click.option("--xstart", type=float,
help="Set simulation start value. If specified, this overrides the xaxis start "
"value specified in the parsed file.")
@click.option("--xstop", type=float,
help="Set simulation stop value. If specified, this overrides the xaxis stop "
"value specified in the parsed file.")
@click.option("--xsteps", type=int,
help="Set number of steps to simulate between --start and --stop. If specified, "
"this overrides the number of xaxis steps specified in the parsed file.")
@click.option("--xscale", type=click.Choice(["lin", "log"]),
help="Set scaling for the xaxis.")
@click.option("--noxaxis", is_flag=True, default=False, help="Switch off x-axis.")
@click.option("--trace", type=click.Choice(["tem", "cavity", "mismatch", "beams", "gouy",
"coupling", "modechanges", "nodes", "all"]),
multiple=True, help="Show simulation trace results. This option can be specified "
"multiple times. The following values are supported: "
"'tem': list TEM modes used, "
"'cavity': cavity eigenvalues and other parameters, "
"'mismatch': mode mismatch parameters for the initial setup, "
"'beams': beam parameters for every node, "
"'gouy': Gouy phases for all spaces, "
"'coupling': coupling coefficients for all components, "
"'modechanges': mode matching parameter changes during calculations, "
"'nodes': nodes found during cavity tracing, "
"'all': all trace results.")
@click.option("--powers", type=click.Choice(["dc", "carrier", "tem00"]),
multiple=True, help="Show powers (W) at each node in the interferometer. This "
"option can be specified multiple times. The following "
"values are supported: "
"'dc': list dc powers, "
"'carrier': list powers in the f=0 fields, "
"'tem00': list powers in the TEM00 mode.")
@click.option("--maxtem", type=str, help="Set maximum transverse electric mode. Can be either "
"an integer or 'off'.")
@click.option("--phase", type=int, help="Set Gouy phase behaviour.")
@click.option("--retrace", type=click.Choice(["force", "off"]),
help="Set retrace behaviour: 'force' recomputes the Gaussian parameters at each "
"node for every data point, and will trace a cavity even if it is unstable; 'off' "
"switches off retracing even if it normally would be done.")
@click.option("--deriv-h", type=float, help="Set step size for numerical differentiation.")
@click.option("--lambda0", type=str, help="Set reference wavelength (m). Supports SI prefixes.")
@click.option("--print-frequencies", is_flag=True, default=False,
help="Print a table listing all the frequencies used in the simulation: carriers, "
"modulation sidebands and signal/quantum sidebands.")
@click.option("--print-noises", is_flag=True, default=False,
help="Print a list of all the quantum noise sources being considered in the "
"simulation and at which components and nodes it is present.")
@click.option("--ignore-block", "ignored_blocks", multiple=True,
help="Ignore the specified block. Can be specified multiple times.")
@click.option("--plot/--no-plot", default=True, show_default=True,
help="Display results as figure.")
@click.option("--save-figure", type=click.File("wb", lazy=False),
help="Save image of figure to file.")
@click.option("--display-graph", is_flag=True, help="Generate and display model node graph "
"using default system document viewer.")
@click.option("--save-input", is_flag=True, default=False,
help="Save generated Finesse input file.")
@click.option("--save-output", is_flag=True, default=False,
help="Save generated Finesse output file.")
@click.option("--finesse-dir", type=click.Path(exists=True, file_okay=False, dir_okay=True),
envvar='FINESSE_DIR', help="Path to directory containing the Finesse 'kat' "
"executable. If not specified, the environment variable FINESSE_DIR is used.")
@click.version_option(version=__version__, prog_name="Pykat")
def cli(file, simulate, xstart, xstop, xsteps, xscale, noxaxis, trace, powers, maxtem, phase,
retrace, deriv_h, lambda0, print_frequencies, print_noises, ignored_blocks, plot,
save_figure, display_graph, save_input, save_output, finesse_dir):
"""Base CLI command group"""
if finesse_dir is None:
# Use default as required by kat object.
finesse_dir = ""
kat = katparser(katdir=finesse_dir)
kat.load(file.name)
has_xaxis = hasattr(kat, "xaxis") and not noxaxis
if ignored_blocks:
for block in ignored_blocks:
kat.removeBlock(block)
if display_graph:
from .tools.plotting.graph import NodeGraph
nodegraph = NodeGraph(kat)
nodegraph.view_pdf()
if simulate:
# Default flag for showing simulation output. Overridden below as necessary.
output_to_show = False
if xstart is not None or xstop is not None or xsteps is not None or xscale is not None:
if not has_xaxis:
click.echo("Limits can only be overridden when an xaxis is defined in FILE and "
"when --noxaxis is unset.", err=True)
sys.exit(1)
# Override xaxis.
limits = kat.xaxis.limits
set_limits = False
if xstart is not None:
limits[0] = xstart
set_limits = True
if xstop is not None:
limits[1] = xstop
set_limits = True
if xsteps is not None:
kat.xaxis.steps = xsteps
if xscale is not None:
kat.xaxis.scale = xscale
if set_limits:
kat.xaxis.limits = np.array(limits).astype(float)
if not has_xaxis:
kat.noxaxis = True
if save_figure is not None:
click.echo("Cannot plot or save figure without an xaxis defined in FILE.",
err=True)
sys.exit(1)
if maxtem is not None:
kat.maxtem = maxtem
if phase is not None:
kat.phase = phase
if retrace is not None:
kat.retrace = retrace
if deriv_h is not None:
kat.deriv_h = deriv_h
if lambda0 is not None:
kat.lambda0 = lambda0
if print_frequencies:
kat.parse("frequency")
output_to_show = True
if print_noises:
kat.parse("printnoises")
output_to_show = True
if trace:
traceval = 0
if "all" in trace:
traceval = 255
else:
traceints = {"tem": 1, "cavity": 2, "mismatch": 4, "beams": 8,
"gouy": 16, "coupling": 32, "modechanges": 64, "nodes": 128}
for tracetype in trace:
traceval |= traceints[tracetype]
kat.trace = traceval
output_to_show = True
if powers:
powerval = 0
powerints = {"dc": 1, "carrier": 2, "tem00": 4}
for powertype in powers:
powerval |= powerints[powertype]
kat.parse("powers %i" % powerval)
output_to_show = True
results = kat.run(save_output=save_output, save_kat=save_input)
if output_to_show and results.rundata:
click.echo(results.rundata)
if has_xaxis and (plot or save_figure is not None):
results.plot(show=plot, filename=save_figure)
elif not output_to_show and not display_graph:
click.echo("No output requested.")
......@@ -365,16 +365,22 @@ class KatRun(object):
def __init__(self):
self._unfreeze()
self.runtime = None
self.save_input = False
self.save_output = False
self.StartDateTime = datetime.datetime.now()
self.x = None
self.stdout = None
self.__stdout = None
self.stderr = None
self.__rundata = None
self.runDateTime = None
self.y = None
self.xlabel = None
self.ylabels = None
self.infile = None
self.outfile = None
self.katScript = None
self.katVersion = None
self.katPath = None
self.yaxis = None
self._freeze()
......@@ -634,12 +640,37 @@ class KatRun(object):
fig.savefig(filename)
if show:
pyplot.show(fig)
pyplot.show()
pyplot.ion()
if return_fig:
return fig
@property
def stdout(self):
"""Standard output from Finesse."""
return self.__stdout
@stdout.setter
def stdout(self, stdout):
self.__stdout = stdout
# Extract run data from stdout.
# Remove everything above the second occurrance of a line with
# 72 dashes (the bottom of the Finesse banner).
rundata = stdout.split("-" * 72)
if len(rundata) <= 2:
# No dashes found - nothing to do.
rundata = ""
else:
rundata = "".join(rundata[2:])
self.__rundata = rundata
@property
def rundata(self):
"""Contents of stdout with the Finesse banner removed."""
return self.__rundata
def saveKatRun(self, filename):
with open(filename,'w') as outfile:
pickle.dump(self, outfile)
......@@ -1030,6 +1061,7 @@ class kat(object):
self.deriv_h = None
self.scale = None
self.__trace = None
self.__powers = None
self.__phase = None
self.__maxtem = None
self.__noxaxis = False
......@@ -1205,6 +1237,11 @@ class kat(object):
else:
self.__maxtem = int(value)
@property
def powers(self): return self.__powers
@powers.setter
def powers(self,value): self.__powers = int(value)
@property
def phase(self): return self.__phase
@phase.setter
......@@ -1671,6 +1708,11 @@ class kat(object):
elif(first == "lambda"):
v = line.split()
self.lambda0 = SIfloat(v[-1])
elif(first == "powers"):
v = line.split()
if len(v) != 2:
raise pkex.BasePyKatException("powers command `{0}` is incorrect.".format(line))
self.powers = int(v[1])
elif(first == "yaxis"):
v = line.split(" ", 1)
self.yaxis = v[-1]
......@@ -2065,6 +2107,8 @@ class kat(object):
r.katScript = "".join(self.generateKatScript())
r.katScript += "time\n"
r.katPath = kat_exec
if (plot==None):
# ensure we don't do any plotting. That should be handled
# by user themselves
......@@ -2081,7 +2125,6 @@ class kat(object):
katfile = open( filepath, 'w' )
katfile.writelines(r.katScript)
katfile.flush()
pipe_name = katfile.name + str(uuid.uuid4())
......@@ -2164,7 +2207,7 @@ class kat(object):
(tag, line) = v
if tag == "version":
r.katVersion = line
r.katVersion = line.strip()
elif tag == "progress" and self.verbose:
var = line.split("\t")
......@@ -2219,6 +2262,8 @@ class kat(object):
path = os.path.split(katfile.name)[0]
outfile = root[0] + ".out"
if self.verbose: print("Used Finesse %s at %s" % (r.katVersion, r.katPath))
traceData = None
if getTraceData:
......@@ -2275,20 +2320,6 @@ class kat(object):
finally:
ifile.close()
if save_output:
newoutfile = "{0}.out".format(base)
cwd = os.path.os.getcwd()
newoutfile = os.path.join(cwd,newoutfile)
if os.path.isfile(newoutfile):
os.remove(newoutfile)
os.rename(outfile, newoutfile)
if self.verbose: print ("\nOutput data saved to '{0}'".format(newoutfile))
# can't see why this check is needed, causes problems when only detectors
# not parsed as pykat objects are used
#if len(self.detectors.keys()) > 0:
......@@ -2307,6 +2338,25 @@ class kat(object):
r.ylabels = [s.strip() for s in hdr[1:]]
#r.ylabels = map(str.strip, hdr[1:]) // replaced 090415 adf
r.save_output = save_output
if save_output:
newoutfile = "{0}.out".format(base)
cwd = os.path.os.getcwd()
newoutfile = os.path.join(cwd,newoutfile)
if os.path.isfile(newoutfile):
os.remove(newoutfile)
os.rename(outfile, newoutfile)
r.outfile = newoutfile
if self.verbose: print("Output data saved to '{0}'".format(newoutfile))
r.save_input = save_kat
if save_kat:
if kat_name is None:
kat_name = "pykat_output"
......@@ -2319,7 +2369,9 @@ class kat(object):
os.rename(katfile.name, newkatfile)
if self.verbose: print ("Kat file saved to '{0}'".format(newkatfile))
r.infile = newkatfile
if self.verbose: print("Kat file saved to '{0}'".format(newkatfile))
katfile.close()
perfData = []
......@@ -2770,6 +2822,7 @@ class kat(object):
pkex.BasePyKatException("Couldn't understand vacuum input list")
if self.scale != None and self.scale !='': out.append("scale {0}\n".format(self.scale))
if self.powers != None: out.append("powers {0}\n".format(self.powers))
if self.phase != None: out.append("phase {0}\n".format(self.phase))
if self.trace != None: out.append("trace {0}\n".format(self.trace))
if self.maxtem != None:
......
"""Model node graph visualisation with Graphviz.
Based on node_graph project by Sebastian Steinlechner: https://github.com/sestei/node_graph/
"""
import tempfile
from ...components import laser, space
from graphviz import Digraph
class NodeGraph(object):
"""Pykat node graph plotter."""
def __init__(self, kat):
self.kat = kat
def view_pdf(self):
"""View the graph as a PDF"""
return self.node_graph().view(directory=tempfile.gettempdir(), cleanup=True)
def _repr_svg_(self):
"""Graphviz rendering for Jupyter notebooks."""
return self.node_graph()._repr_svg_()
def node_graph(self, engine="fdp", node_style="filled", node_font="Helvetica",
node_font_size=10, node_width=0.2, node_shape="point", node_color="red",
node_dump_color="black", edge_arrowhead="dot", graph_font="Helvetica",
graph_font_size=8, component_color="LightSkyBlue", laser_color="Orange",
space_color="MediumSlateBlue", detector_color="YellowGreen"):
"""Create Graphviz node graph"""
graph = Digraph(engine=engine)
graph.attr("node", style=node_style, fontname=node_font, fontsize=str(node_font_size))
graph.attr("edge", arrowhead=edge_arrowhead)
graph.attr("graph", fontname=graph_font,
fontsize=str(graph_font_size))
def add_kat_node(from_id, node):
node_id = str(node.id)
color = node_dump_color if node.name == 'dump' else node_color
graph.node(node_id, label='', xlabel=node.name, width=str(node_width),
shape=node_shape, color=color)
graph.edge(from_id, node_id)
for comp in self.kat.components.values():
if isinstance(comp, laser):
attr = {'fillcolor': laser_color, 'shape': 'box'}
elif isinstance(comp, space):
attr = {'fillcolor': space_color, 'shape': 'diamond'}
else:
attr = {'fillcolor': component_color, 'shape': 'box'}
graph.node(comp.name, **attr)
for node in comp.nodes:
add_kat_node(comp.name, node)
for det in self.kat.detectors.values():
if len(det._nodes) > 0:
graph.node(det.name, fillcolor=detector_color, shape='ellipse')
for node in det._nodes:
add_kat_node(det.name, node)
return graph
\ No newline at end of file
......@@ -6,3 +6,5 @@ h5py
pandas
matplotlib
tabulate
click >= 7.0
graphviz
\ No newline at end of file
......@@ -49,5 +49,10 @@ setup(
'ifo/adv/files/*.kat',
'ifo/voyager/files/*.kat',
'style/*.mplstyle']},
include_package_data=True
include_package_data=True,
entry_points={
'console_scripts': [
'pykat = pykat.__main__:cli'
],
},
)
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