Compound parameters
This is related to #133 (closed), #169
A related merge request !30 (closed) has been closed as it was too much of a work-around without addressing the real issue.
Commit 7984fe0d changed Surface.Rc
to always return a NumPy array of Surface.Rcx, Surface.Rcy
- similar to the behaviour of all the geometric properties of the Cavity
class. I see this as a temporary fix where the real solution should be to implement a CompoundParameter
class which acts similarly to the existing Parameter
but is also a container around multiple, individual model parameters.
There are some comments on this in #133 (closed) but the key problems this would resolve would be to allow direct scanning of Surface.Rc
without needing to link Rcx
and Rcy
with refs; this will also apply to Lens.f
when I change this to have fx
and fy
parameters. It would also help to avoid any confusion over the "types" of Rc
and f
- i.e. they would also
be parameters, not properties of their respective classes.
Implementing such a CompoundParameter
class is non-trivial, however, so this may need to wait a while - and might require some re-organising of the Parameter
code. I'll just dump some thoughts on how this may end up being implemented, in code form, below for now in case it's useful in the future.
# finesse/elements.pxd
# maybe shouldn't derive from Parameter as it is currently,
# could be better to have a BaseParameter class from which
# both Parameter and CompoundParameter derive
cdef class CompoundParameter(Parameter):
cdef:
np.ndarray __params # The array of Parameter objects
object[::1] __params_view # A view on above for efficiency
Py_ssize_t __N # No. of params that are coupled (i.e. 2 for RoCs, focal length)
bint __coupled # are all values the same
# finesse/elements.pyx
cdef class CompoundParameter(Parameter):
def __init__(self, name, *args):
value = args[0].value
units = args[0].units
flag = args[0].rebuild
component = args[0].component
super().__init__(name, value, units, flag, component)
self.__params = np.array(args)
self.__params_view = self.__params
self.__N = len(self.__params)
self.__coupled = True
cdef void set_double_value(self, double value):
cdef Py_ssize_t i
cdef Parameter p
for i in range(self.__N):
p = self.__params_view[i]
p.set_double_value(value)
self.__coupled = True
@property
def value(self):
if self.__coupled:
return self.__params_view[0].value
return np.array([p.value for p in self.__params])
@value.setter
def value(self, value):
cdef Py_ssize_t i
if is_iterable(value):
if len(value) != self.__N:
raise ValueError(
f"Expected length of iterable to be {self.__N} "
f"but got {len(value)}"
)
for i in range(self.__N):
self.__params_view[i].value = value[i]
self.__coupled = False
else:
for i in range(self.__N):
self.__params_view[i].value = value
self.__coupled = True
def compound_parameter(name, *args):
if len(args) < 2:
raise ValueError("Excpected at least two dependent parameters.")
def func(cls):
p = _parameter(
lambda x: getattr(x, f"__param_{name}"),
lambda x, v: getattr(x, f"__param_{name}")._set(v),
lambda x: getattr(x, f"__param_{name}").locked,
)
cls._compound_param_dict[cls].append((name, args))
setattr(cls, name, p)
return cls
return func
# Could then be used in e.g. finesse/components/mirror.py via
@compound_parameter("Rc", "Rcx", "Rcy")
@model_parameter(
"Rcx", np.inf, Rebuild.HOM, units="m", validate="_check_Rc", is_geometric=True,
)
@model_parameter(
"Rcy", np.inf, Rebuild.HOM, units="m", validate="_check_Rc", is_geometric=True,
)
class Mirror(Surface):
# ...
There's obviously more thought to go into this, but I believe that having a CompoundParameter
to wrap around RoCs, focal lengths should provide a neat interface for this stuff (hopefully).