Commit e1485de0 authored by Sean Leavey's avatar Sean Leavey
Browse files

Merge branch 'develop' into fix/build-tooling

parents 4b01be8d 23f82dff
Pipeline #239099 failed with stages
in 2 minutes
......@@ -6,9 +6,8 @@ This file is part of the interferometer simulation Finesse 3
<http://www.gwoptics.org/finesse>
Copyright (C) 2019 onwards Andreas Freise, Daniel Brown,
Philip Jones, Sam Rowlinson and Sean Leavey, with
contributions to earlier versions of Finesse from Paul
Cochrane, and Gerhard Heinzel.
Philip Jones, Sam Rowlinson and Sean Leavey. Other contributions
to the Finesse project are linked in the documentation.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
......
### NOTE See https://finesse.docs.ligo.org/finesse3/developer/codeguide/requirements.html
# Core
-r requirements.txt
cython == 0.29.23
### NOTE See https://finesse.docs.ligo.org/finesse3/developer/codeguide/requirements.html
# Core
-r requirements.txt
# Build
-r requirements-build.txt
# Tests
-r requirements-test.txt
# Docs
-r requirements-doc.txt
# Extras
black
pre-commit
pylint
flake8
flake8-bugbear
# Requirements for building documentation
### NOTE See https://finesse.docs.ligo.org/finesse3/developer/codeguide/requirements.html
sphinx
sphinx_rtd_theme
sphinxcontrib-bibtex
sphinxcontrib-katex
sphinxcontrib-svg2pdfconverter
sphinxcontrib-programoutput
jupyter-sphinx
numpydoc
reslate
# Requirements for running tests
### NOTE See https://finesse.docs.ligo.org/finesse3/developer/codeguide/requirements.html
pytest
pytest-cov
coverage == 4.5.4 # Can't use latest version due to Cython bug: https://github.com/cython/cython/issues/3515
pycobertura
Faker
hypothesis
### NOTE See https://finesse.docs.ligo.org/finesse3/developer/codeguide/requirements.html
numpy >= 1.20.0 # 1.19 causes a segfault, 1.20 is not available on conda so we must specify it here, see #306
scipy >= 1.4.0
matplotlib >= 3.0.0
networkx >= 2.4
sly == 0.4
h5py >= 2.10.0
click >= 7.1.0
click-default-group >= 1.2.2
tabulate >= 0.8.7
control >= 0.9.0
sympy >= 1.6.0
......@@ -17,6 +17,7 @@ Author: Sean Leavey
import sys
import argparse
import inspect
from textwrap import indent as _indent, wrap
from pathlib import Path
from importlib import import_module
import warnings
......@@ -24,7 +25,11 @@ from numpydoc.docscrape import FunctionDoc, ClassDoc
import finesse
indent = lambda text: _indent("\n".join(wrap(str(text))) + "\n", prefix=" " * 4)
FINESSE_ROOT = Path(finesse.__file__.replace("__init__.py", ""))
DOCURL = "https://finesse.docs.ligo.org/finesse3/developer/documenting.html#writing-sphinx-compatible-docstrings"
def check_module(path):
......@@ -47,52 +52,124 @@ def check_module(path):
has_issue = False
# Categorise members.
classes = []
functions = []
for _, obj in inspect.getmembers(module):
if not inspect.isclass(obj):
try:
if obj.__module__ != module.__name__:
# Reject imported modules.
continue
except AttributeError:
# This is not a class or function; maybe a dict or something else defined on
# the top level that we don't care about.
continue
if obj.__module__ != module.__name__:
# Reject imported modules.
if inspect.isclass(obj):
classes.append(obj)
elif inspect.isfunction(obj):
functions.append(obj)
else:
warnings.warn(f"don't know how to handle member {repr(obj)}")
continue
clsobj = f"{module_path}::{obj.__name__}"
initmeth = None
try:
if obj.__init__.__module__ == obj.__module__:
# There is an init method.
initmeth = obj.__init__
except AttributeError:
# Probably a C parent.
pass
for class_ in classes:
classname = f"{module_path}::{class_.__name__}"
# Parse docstring.
initdoc = None
with warnings.catch_warnings(record=True) as warnlist:
# Import classdoc so that it triggers warnings caught by the context manager.
# Create a ClassDoc object so that we can grab any warnings issued by
# numpydoc.
try:
ClassDoc(obj)
if initmeth:
initdoc = FunctionDoc(initmeth)
ClassDoc(class_)
except Exception as e:
print(f"error while processing docstring for {obj}: {e}")
print(
f"error while processing docstring for {class_.__name__}: \n{indent(e)}"
)
has_issue = True
# Print any caught warnings.
if warnlist:
for wrng in warnlist:
print(f"{clsobj}: numpydoc warning: {wrng.message}")
print(f"{classname}: numpydoc warning: {wrng.message}")
has_issue = True
if initdoc and initdoc.get("Parameters"):
print(
f"{clsobj}: constructor parameters should be documented in the class "
"docstring, not __init__ (see https://finesse.readthedocs.io/en/latest/developer/documenting.html#writing-sphinx-compatible-docstrings)."
)
# Create FunctionDoc objects for each of the class methods to grab any
# warnings issued by numpydoc.
for _, method in inspect.getmembers(class_):
try:
methodname = method.__name__
methodmodule = method.__module__
except AttributeError:
# Maybe a property?
try:
methodname = method.fget.__name__
methodmodule = method.fget.__module__
except AttributeError:
continue
if methodmodule is not class_.__module__:
# This is an inherited member, which we'll skip.
continue
if methodname.startswith("__"):
# Skip dunder methods.
continue
methodpath = f"{classname}.{methodname}"
with warnings.catch_warnings(record=True) as warnlist:
try:
methoddoc = FunctionDoc(method)
except Exception as e:
print(
f"error while processing docstring for "
f"{class_.__name__}.{method.__name__}:\n{indent(e)}"
)
has_issue = True
else:
# Detect if the method is the current class's init method.
try:
isinit = method is class_.__init__
except AttributeError:
# Probably a C parent.
isinit = False
if isinit:
# Check whether the __init__ documentation incorrectly lists the
# init parameters.
if methoddoc and methoddoc.get("Parameters"):
print(
f"{methodpath}: constructor parameters should be "
f"documented in the class docstring, not __init__ "
f"(see {DOCURL})."
)
has_issue = True
# Print any caught warnings.
if warnlist:
for wrng in warnlist:
print(f"{methodpath}: numpydoc warning: {wrng.message}")
has_issue = True
for function in functions:
functionname = f"{module_path}::{function.__name__}"
has_issue = True
with warnings.catch_warnings(record=True) as warnlist:
try:
FunctionDoc(function)
except Exception as e:
print(f"error while processing docstring for {function}: \n{indent(e)}")
has_issue = True
# Print any caught warnings.
if warnlist:
for wrng in warnlist:
print(f"{functionname}: numpydoc warning: {wrng.message}")
has_issue = True
return 1 if has_issue else 0
......
......@@ -822,7 +822,7 @@ class RunLocks(Action):
if self.exception_on_fail:
raise Exception("Locks failed: max iterations reached")
else:
LOGGER.warn("Locks failed")
LOGGER.warning("Locks failed")
for lock, value in zip(locks, initial_parameters):
lock.feedback.value = value
return sol
......@@ -1327,7 +1327,7 @@ class SensingMatrixSolution(BaseSolution):
readouts=None,
tablefmt="pandas",
floatfmt=".2G",
highlight="dof",
highlight=None,
highlight_color="#FFD54F",
):
"""Displays a HTML table of the sensing matrix, with the largest absolute value
......@@ -1350,15 +1350,15 @@ class SensingMatrixSolution(BaseSolution):
falling back to 'html'.
floatfmt : str, optional
Format to print numbers in, defaults to '.2G'.
highlight : str, optional
highlight : str or None, optional
Either 'dof' to highlight the readout that gives the largest
output for each dof, or 'readout' to highlight the dof for
which each readout gives the largest output. Defaults to
'dof'.
None (no highlighting).
highlight_color : str, optional
Color to highlight the maximum values with, or None for no
highlighting. Pandas is required for this to have an effect.
Defaults to pale orange.
Color to highlight the maximum values with. Pandas is
required for this to have an effect. Defaults to pale
orange.
"""
from IPython.display import display
......@@ -1379,19 +1379,21 @@ class SensingMatrixSolution(BaseSolution):
"",
)
B = pd.DataFrame(B, index=dofs, columns=readouts)
if highlight == "dof":
axis = 1
style = B.style.apply(highlight_max, axis=1)
elif highlight == "readout":
axis = 0
style = B.style.apply(highlight_max, axis=0)
elif highlight is None:
style = B.style
else:
raise ValueError(
"Argument 'highlight' must be one of 'dof' or 'readout'."
"Argument 'highlight' must be one of 'dof', 'readout' or None."
)
B = pd.DataFrame(B, index=dofs, columns=readouts)
display(
B.style.apply(highlight_max, axis=axis).format("{:" + floatfmt + "}")
)
display(style.format("{:" + floatfmt + "}"))
elif tablefmt == "html":
display(
tabulate(
......@@ -1739,7 +1741,8 @@ class OptimizeSolution(BaseSolution):
class Optimize(Action):
"""An action that will optimize the value of `parameter` to either maximize or
minimize the output of `detector`.
minimize the output of `detector`. Extra keyword arguments are passed on to
`scipy.optimize.minimize`.
Parameters
----------
......@@ -1778,6 +1781,7 @@ class Optimize(Action):
max_iterations=10000,
accuracy=None,
name="maximize",
**kwargs,
):
super().__init__(name)
self.detector = detector
......@@ -1787,6 +1791,7 @@ class Optimize(Action):
self.kind = kind
self.max_iterations = max_iterations
self.accuracy = accuracy
self.kwargs = kwargs
def _do(self, state):
from scipy.optimize import minimize
......@@ -1835,9 +1840,10 @@ class Optimize(Action):
func,
np.array([param.value]),
tol=self.accuracy,
bounds=[self.bounds],
bounds=[self.bounds] if self.bounds else None,
options={"maxiter": self.max_iterations},
callback=callback,
**self.kwargs,
)
if self.offset == 0:
print(
......@@ -1856,7 +1862,7 @@ class Optimize(Action):
class Minimize(Optimize):
"""An action that will optimize the value of `parameter` to minimize the output of
`detector`.
`detector`. Extra keyword arguments are passed on to `scipy.optimize.minimize`.
Parameters
----------
......@@ -1888,7 +1894,7 @@ class Minimize(Optimize):
class Maximize(Optimize):
"""An action that will optimize the value of `parameter` to maximize the output of
`detector`.
`detector`. Extra keyword arguments are passed on to `scipy.optimize.minimize`.
Parameters
----------
......@@ -1940,7 +1946,7 @@ class NoiseProjectionSolution(BaseSolution):
ax.legend((*np.array(self.noises)[noises_to_plot], "Total"))
ax.set_ylim(*rng)
else:
LOGGER.warn("No noise data to plot in this solution")
LOGGER.warning("No noise data to plot in this solution")
ax.set_ylabel(
f"ASD [{output_node if not self.scaling else self.scaling}/$\\sqrt{{\\mathrm{{Hz}}}}$]"
......
......@@ -99,7 +99,7 @@ cdef class CCSMatrix:
cpdef set_rhs(self, SuiteSparse_long index, complex_t value, unsigned rhs_index=?)
cdef int c_set_rhs(self, SuiteSparse_long index, complex_t value, Py_ssize_t rhs_index) except -1
cdef unsigned request_rhs_view(self)
cdef complex_t[::1] get_rhs_view(self, unsigned index)
cpdef complex_t[::1] get_rhs_view(self, unsigned index)
cpdef construct(self, complex_t diagonal_fill=?)
cdef np.ndarray get_matrix(self, SuiteSparse_long _from, SuiteSparse_long _to, complex_t** start_ptr, SuiteSparse_long* from_rhs_index)
cpdef clear_rhs(self, unsigned rhs_index=?)
......@@ -124,8 +124,8 @@ cdef class SubCCSView:
complex_t* ptr
np.ndarray A
readonly complex_t[:, ::1] from_rhs_view # rhs[rhs index, rhs values]
tuple __shape
tuple __strides
readonly Py_ssize_t from_rhs_view_size
complex_t[::1] prop_za_zm_workspace
cdef void fill_za(self, complex_t a)
cdef void fill_zd(self, complex_t[::1] D)
......@@ -155,9 +155,9 @@ cdef class SubCCSView:
cdef void fill_za_zmvc(self, complex_t a, DenseZMatrix* M, DenseZVector* V)
cdef void fill_negative_za_zmvc(self, complex_t a, DenseZMatrix* M, DenseZVector* V)
cdef void fill_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, complex_t[:,::1] M, bint increment)
cdef void fill_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, DenseZMatrix* M, bint increment)
cdef void fill_prop_za(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, bint increment)
cdef void fill_neg_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, complex_t[:,::1] M, bint increment)
cdef void fill_neg_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, DenseZMatrix* M, bint increment)
cdef void fill_neg_prop_za(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, bint increment)
......
# cython profile=True
# cython: profile=True
"""
Sparse matrix objects with factorisation and solving routines performed via KLU.
"""
......@@ -407,7 +407,7 @@ cdef class CCSMatrix:
else:
return rtnD
cdef complex_t[::1] get_rhs_view(self, unsigned index):
cpdef complex_t[::1] get_rhs_view(self, unsigned index):
"""
Returns a view of the rhs vector corresponding to `index`.
......@@ -431,17 +431,43 @@ cdef class CCSMatrix:
Returns
-------
sparse : list(tuple())
It is a list of (col, row, value)
data : list[complex]
Value of each non-zero element
rows : list[complex]
Row index of each non-zero element
cols : list[complex]
Column index of each non-zero element
"""
vals = []
data = []
rows = []
cols = []
ccol = -1
for i in range(self.__nnz):
if self.col_ptr[ccol+1] == i:
ccol += 1
vals.append((ccol, self.row_idx[i], self.values[i]))
data.append(self.values[i])
cols.append(ccol)
rows.append(self.row_idx[i])
return data, rows, cols
def to_scipy_coo(self):
from scipy.sparse import coo_matrix
data, rows, cols = self.get_matrix_elements()
return coo_matrix((data, (rows, cols)), dtype=complex)
return vals
def to_scipy_csr(self):
from scipy.sparse import csr_matrix
data, rows, cols = self.get_matrix_elements()
return csr_matrix((data, (rows, cols)), dtype=complex)
def to_scipy_csc(self):
from scipy.sparse import csc_matrix
data, rows, cols = self.get_matrix_elements()
return csc_matrix((data, (rows, cols)), dtype=complex)
def print_matrix(self):
"""Print a view of the non-zero elements in this matrix."""
......@@ -471,7 +497,7 @@ cdef class CCSMatrix:
print(" (col, row) = value")
ccol = -1
longest = np.zeros(2, dtype=np.int)
longest = np.zeros(2, dtype=int)
for i in range(self.__nnz):
if self.col_ptr[ccol+1] == i:
ccol += 1
......@@ -515,7 +541,7 @@ cdef class CCSMatrix:
print(f"Vector {self.name}: neqs={self.num_eqs}")
print(" (row) = value")
longest = np.zeros(2, dtype=np.int)
longest = np.zeros(2, dtype=int)
for i in range(self.num_eqs):
longest[0] = max(longest[0], len(f"({i})"))
longest[1] = max(longest[1], len(f"{rhs[i]}"))
......@@ -560,8 +586,6 @@ cdef class SubCCSView:
self.A = None
self._from = _from
self._to = _to
self.__shape = None
self.__strides = None
self.conjugate_fill = conjugate_fill
@property
......@@ -572,7 +596,11 @@ cdef class SubCCSView:
@property
def shape(self):
return self.__shape
return (self.size1, self.size2)
@property
def strides(self):
return (self.stride1, self.stride2)
@property
def view(self):
......@@ -600,10 +628,12 @@ cdef class SubCCSView:
self.size2 = self.A.shape[1]
self.stride2 = self.A.strides[1]//16
self.from_rhs_view = m.rhs_view[:, self.from_rhs_index:(self.from_rhs_index+self.size2)]
self.from_rhs_view_size = self.size2
else:
self.size2 = 0
self.stride2 = 0
self.from_rhs_view = m.rhs_view[:, self.from_rhs_index:(self.from_rhs_index+self.size1)]
self.from_rhs_view_size = self.size1
cdef void fill_za(self, complex_t a):
raise NotImplementedError()
......@@ -677,10 +707,10 @@ cdef class SubCCSView:
cdef void fill_negative_zm(self, complex_t[:,::1] M):
raise NotImplementedError()
cdef void fill_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, complex_t[:,::1] M, bint increment):
cdef void fill_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, DenseZMatrix* M, bint increment):
raise NotImplementedError()
cdef void fill_neg_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, complex_t[:,::1] M, bint increment):
cdef void fill_neg_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, DenseZMatrix* M, bint increment):
raise NotImplementedError()
cdef void fill_prop_za(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, bint increment):
......@@ -716,6 +746,7 @@ cdef class SubCCSMatrixView(SubCCSView):
@cython.wraparound(False)
@cython.initializedcheck(False)
cdef void fill_zm(self, complex_t[:,::1] M):
assert(M.shape[0] == self.size1 and M.shape[1] == self.size2)
cdef Py_ssize_t i, j
if self.conjugate_fill:
for i in range(self.size1):
......@@ -730,6 +761,7 @@ cdef class SubCCSMatrixView(SubCCSView):
@cython.wraparound(False)
@cython.initializedcheck(False)
cdef void fill_za_zm(self, complex_t a, complex_t[:,::1] M):
assert(M.shape[0] == self.size1 and M.shape[1] == self.size2)
cdef Py_ssize_t i, j
if self.conjugate_fill:
for i in range(self.size1):
......@@ -824,15 +856,20 @@ cdef class SubCCSMatrixView(SubCCSView):
@cython.wraparound(False)
@cython.initializedcheck(False)
cdef void fill_za_zmvc(self, complex_t a, DenseZMatrix* M, DenseZVector* V):
"""Sets view of submatrix to a * M @ V^*
"""Sets view of submatrix to
.. math::
a * (M @ V^*)
"""
cdef int i, k, stride
assert(self.size1 == 1 or self.size2 == 1) # This view must be some 1D array either col or row
assert(M.size1 == V.size) # Make sure size of matrix is correct for product
assert( # check we vector is right size for this input matrix
assert( # check the vector is right size for this input matrix
(self.size1 == 1 and self.size2 == V.size)
or (self.size2 == 1 and self.size1 == V.size)
)
# Not being too fussed about the shape of the input vector, can
# be either row or column vector
if V.size == self.size1:
stride = self.stride1
else:
......@@ -852,29 +889,123 @@ cdef class SubCCSMatrixView(SubCCSView):
# double* access to imaginary part
((<double*>self.ptr) + 2*i*stride + 1)[0] *= -1
def do_fill_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, complex_t[:,::1] M, bint increment):
cdef DenseZMatrix m
m.ptr = &M[0,0]
m.size1 = M.shape[0]
m.size2 = M.shape[1]
m.stride1 = M.strides[0]//16
m.stride2 = M.strides[1]//16
self.fill_prop_za_zm(V, rhs_idx, a, &m, increment)
# @cython.boundscheck(False)
# @cython.wraparound(False)
# @cython.initializedcheck(False)
# cdef void fill_prop_za_zm(self, SubCCSView V, Py_ssize_t rhs_idx, complex_t a, complex_t[:,::1] M, bint increment):
# # TODO ddb this can be optimised later once I know it works properly
# # For a given view of a matrix, propagate the current solved solution in it
# # then multiply it by some factor and apply some matrix operator to it
# V_rhs = np.atleast_2d(V.from_rhs_view[rhs_idx]).T
# if self.conjugate_fill:
# if increment:
# self.view[:] += np.conj(M @ (a * V.view @ V_rhs))
# else:
# self.view[:] = np.conj(M @ (a * V.view @ V_rhs))
# else: