## 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).