Source code for circadapt.components

"""
Components for easy input/output of the CircAdapt model.

The component is a general class. It holds parameters that will be called
using the Parameter object and signals that are called using the Signals
object.
"""

import numpy as np
from circadapt.settings import __version__

[docs] class Component: """ General functions to communicate with c++ objects. Parameters ---------- model: ctypes object C++ model locs: string Locations of objects in the c++ model that can be altered along this component """ parameters = [] signals = [] parameter_on_set = {} def __init__(self, component_type, model, objects=None): self._component_type = component_type self._model = model self.objects = [] if objects is None else objects self.build()
[docs] def build(self): self.objects_short = np.array([ o.split('.')[-1] for o in self.objects ])
[docs] def add_object(self, o): self.objects.append(o) self.build()
[docs] def __repr__(self): """Object representation in string format.""" return '<' + str(self.__class__.__name__) + '> \n' + \ 'parameters: \n ' + self.parameters.__repr__() + \ '\n\n signals: \n ' + self.signals.__repr__() + \ '\n\n objects: \n ' + self.objects.__repr__() + \ '\n<\\' + str(self.__class__.__name__) + '>'
[docs] def __getitem__(self, arg: any) -> any: """ Get data. This function is called when self[arg]. Parameters ---------- arg: slice or string If slice, return a components object with locs obtained from slice. If str, return the parameter or signal for all locs """ if isinstance(arg, slice) or \ isinstance(arg, int): return self.__class__( self._model, objects=self.objects[arg], ) if arg in self.signals: return Signal(self._model, arg, self.objects) if arg in self.parameters: return Parameter( self._model, arg, self.objects, self.parameter_on_set.get(arg, None), ) raise ValueError('Argument unknown')
[docs] def __setitem__(self, arg: str, val: any) -> any: """ Set data. Set data of the parameter given in arg. Only parameter names can be used. Parameters ---------- arg: str Parameter name val: float/int/bool Value will automatically be translated to type to """ if arg in self.parameters: par = Parameter(self._model, arg, self.objects) par[:] = val # trigger function if arg in self.parameter_on_set: self.parameter_on_set[arg](self._model) return if arg in self.signals: raise ValueError('Can not set signal') raise ValueError(f'Parameter "{arg}" unknown')
[docs] def __iter__(self): """Iterate over object, used for dict(self).""" yield 'objects', self.objects for key in self.parameters + self.signals: value = self.__getitem__(key)._get() yield key, value
[docs] def add(self, name): split_name = name.split('.') return self._model.add_component(self._component_type, split_name[-1], name[:-len(split_name[-1])-1])
[docs] class ParSig: """Basic functions used for Parameter and Signal classes.""" def __init__(self, model, par, locs): self._model = model self._par = par self.objects = locs # Store number of run commmands of circadapt model. # If is changed, values do not belong to model anymore and set function # must be disabled. self._model_i_run = model._count_run_commands self.build()
[docs] def build(self): self.objects_short = np.array([ o.split('.')[-1] for o in self.objects ]) self._values = self._get()
[docs] def __repr__(self): """Object representation in string format.""" return self._values.__repr__()
[docs] def __add__(self, val): """Handle the + operator.""" if isinstance(val, Signal): val = val[:] return self._values + val
[docs] def __radd__(self, val): """Handle the + operator as right hand element.""" return val + self._values
[docs] def __sub__(self, val): """Handle the - operator.""" if isinstance(val, Signal): val = val[:] return self._values - val
[docs] def __rsub__(self, val): """Handle the - operator as right hand element.""" return val - self._values
[docs] def __mul__(self, val): """Handle the * operator.""" if isinstance(val, Signal): val = val[:] return self._values * val
[docs] def __rmul__(self, val): """Handle the * operator as right hand element.""" return val * self._values
[docs] def __truediv__(self, val): """Handle the / operator.""" return self._values / val
[docs] def __rtruediv__(self, val): """Handle the / operator as right hand element.""" return val / self._values
[docs] def __floordiv__(self, val): """Handle the // operator.""" return self._values // val
[docs] def __rfloordiv__(self, val): """Handle the // operator.""" return val // self._values
[docs] def __mod__(self, val): """Handle the % operator.""" return self._values % val
[docs] def __rmod__(self, val): """Handle the % operator.""" return val % self._values
[docs] def __pow__(self, val): """Handle the ** operator.""" return self._values ** val
[docs] def __rpow__(self, val): """Handle the ** operator as right hand element.""" return val ** self._values
[docs] def __neg__(self): """Handle -self.""" return -self._values
[docs] def __lt__(self, val): """Handle the < operator.""" return self._values < np.array(val)
[docs] def __le__(self, val): """Handle the <= operator.""" return self._values <= np.array(val)
[docs] def __gt__(self, val): """Handle the > operator.""" return self._values > np.array(val)
[docs] def __ge__(self, val): """Handle the >= operator.""" return self._values >= np.array(val)
[docs] def __eq__(self, val): """Handle the == operator.""" return self._values == np.array(val)
[docs] def __ne__(self, val): """Handle the != operator.""" return self._values != np.array(val)
[docs] def __getitem__(self, arg: any) -> any: """ Get data. This function is called when self[arg]. """ if isinstance(arg, slice) or isinstance(arg, int): return self._values[arg] if isinstance(arg, str): if np.sum(self.objects_short == arg) == 1: return self._values[self.objects_short == arg][0] if np.any(self.objects_short == arg): # too many raise ValueError('Something went wrong in building the model.') raise ValueError('Object not found. Check the spelling.') if isinstance(arg, list) and isinstance(arg[0], str): if np.any(np.min( self.objects_short != np.array(arg).reshape((-1, 1)), axis=1)): raise ValueError('Not all ojects are found.') return self._values[ np.argmax(self.objects_short == np.array(arg).reshape((-1, 1)), axis=1) ] if (isinstance(arg, list) and isinstance(arg[0], int)): return self._values[arg] if (isinstance(arg, np.ndarray)): return self._values[arg] raise ValueError('Unknown key ', arg)
[docs] class Parameter(ParSig): """ General functions to retreive signals from the c++ object. Parameters ---------- model: ctypes object C++ model par: str Parameter name that will be obtained from each loc. locs: string Locations of objects in the c++ model that can be altered along this component. """ def __init__(self, model, par, locs, parameter_on_set=None): self.parameter_on_set = parameter_on_set self.shape = len(locs), super().__init__(model, par, locs) def _get(self, arg=slice(None, None, None)): """ Get data. This function is called when self[arg]. Parameters ---------- arg: slice or string If slice, return a components object with locs obtained from slice. If str, return the parameter or signal for all locs """ if isinstance(arg, slice): return np.array([self._model.get(loc+'.'+self._par) for loc in self.objects[arg]]) return self._model.get(self.objects[arg]+'.'+self._par)
[docs] def __len__(self): """Handle len(self).""" return self.shape[0]
[docs] def __setitem__(self, arg, value): """ Set data. Set data of the parameter for locs given in arg. Only parameter names can be used. Parameters ---------- arg: slice Locations to be changed. val: float/int/bool Value will automatically be translated to type of the parameter. """ if self._model_i_run != self._model._count_run_commands: raise ReferenceError('Parameters stored seperatly can not be ' 'changed.') if not hasattr(value, '__len__') and isinstance(arg, slice): value = np.ones(len(self.objects[arg]))*value # check for length if isinstance(arg, slice) and (len(self.objects[arg]) != len(value)): raise ValueError('Dimensions do not match.') if isinstance(arg, slice): for i_loc, loc in enumerate(self.objects[arg]): self._model.set(loc+'.'+self._par, value[i_loc]) elif isinstance(arg, list) and isinstance(arg[0], str): value = np.ones(len(arg))*value for i_loc, loc in enumerate(arg): fullloc = self.objects[ np.argwhere(self.objects_short == loc)[0, 0]] self._model.set(fullloc+'.'+self._par, value[i_loc]) elif isinstance(arg, np.ndarray) and arg.dtype == 'bool': # if value is scalar, map to same length # if value is array, it needs to have same length as arg set_values = np.ones(np.sum(arg)) * value set_idx = np.argwhere(arg).reshape(-1) for i, j in enumerate(set_idx): fullloc = self.objects[j] self._model.set(fullloc+'.'+self._par, set_values[i]) elif isinstance(arg, str): if np.sum(self.objects_short == arg) == 1: fullloc = self.objects[np.argwhere( self.objects_short == arg)[0, 0]] self._model.set(fullloc+'.'+self._par, value) elif np.any(self.objects_short == arg): # too many raise ValueError('Something went wrong in building the model.') else: raise ValueError('Object not found. Check the spelling.') elif hasattr(arg, '__len__') and hasattr(value, '__len__'): for a, v in zip(arg, value): self.__setitem__(a, v) elif hasattr(arg, '__len__'): for a in arg: self.__setitem__(a, value) else: self._model.set(self.objects[arg]+'.'+self._par, value) if self.parameter_on_set is not None: self.parameter_on_set(self._model)
[docs] def __getitem__(self, *arg): return super().__getitem__(*arg)
[docs] class Signal(ParSig): """ General functions to retreive signals from the c++ object. Parameters ---------- model: ctypes object C++ model par: str Parameter name that will be obtained from each loc. locs: string Locations of objects in the c++ model that can be altered along this component """ def __init__(self, model, par, locs): super().__init__(model, par, locs) val0 = self._model.get(locs[0]+'.' + par) # shape in line with numpy arrays if hasattr(val0, '__len__'): self.shape = len(val0), len(locs) else: self.shape = len(locs), # ndim in line with numpy arrays self.ndim = len(self.shape)
[docs] def __len__(self): """Handle len(self).""" return self.shape[-1]
def _get(self, arg=slice(None, None, None)): """Get the signal data for all locs.""" ret = np.concatenate(( [[self._model.get(loc+'.'+self._par)] for loc in self.objects] ), axis=0)[arg].T return ret
[docs] def __setitem__(self, arg: any, value) -> any: """ Set data. Function is called when use self[arg]. Raises ------ Always raise error, signals can not be changed. """ raise ValueError('Can not change signals')
[docs] def __getitem__(self, arg: any) -> any: """ Get data. This function is called when self[arg]. Parameters ---------- arg: slice or string If slice, return a components object with locs obtained from slice. If str, return the parameter or signal for all locs """ if (isinstance(arg, tuple) and isinstance(arg[1], list) and isinstance(arg[1][0], str)): idx_objects = np.argmax( self.objects_short == np.array(arg[1]).reshape((-1, 1)), axis=1, ) return self._values[:, idx_objects][arg[0], :] if isinstance(arg, tuple) and isinstance(arg[1], str): idx_obj = self.objects_short == arg[1] if not np.any(idx_obj): raise ValueError('Object not found.') value = self._values[arg[0], idx_obj] if np.issubdtype(type(arg[0]), np.integer): return value[0] return value.reshape(-1) if isinstance(arg, tuple): return self._values[arg[0], arg[1]] if (isinstance(arg, slice) or isinstance(arg, int)): return self._values[arg] return super().__getitem__(arg)
[docs] class General(Component): """Component object with no locations.""" parameters = { 't_cycle': 'Model.t_cycle', 'version_library': 'Version', 'version_pyCircAdapt': lambda: __version__, } signals = { } def __init__(self, model): self._model = model
[docs] def __repr__(self): """Object representation in string format.""" return '<' + str(self.__class__.__name__) + '> \n' + \ 'parameters: \n ' + self.parameters.keys().__repr__() + \ '\n\n signals: \n ' + self.signals.keys().__repr__() + \ '\n<\\' + str(self.__class__.__name__) + '>'
[docs] def __getitem__(self, arg: any) -> any: """ Get data. This function is called when self[arg]. Parameters ---------- arg: slice or string If slice, return a components object with locs obtained from slice. If str, return the parameter or signal for all locs """ if arg in self.parameters and isinstance(self.parameters[arg], str): return self._model.get(self.parameters[arg]) if arg in self.parameters and callable(self.parameters[arg]): return self.parameters[arg]() if arg in self.signals: return self._model.get(self.signals[arg]) raise ValueError('unknown')
[docs] def __setitem__(self, arg, value): """ Set data. Set data of the parameter for locs given in arg. Only parameter names can be used. Parameters ---------- arg: slice Locations to be changed. val: float/int/bool Value will automatically be translated to type of the parameter. """ if arg in self.parameters: return self._model.set(self.parameters[arg], value) if arg in self.signals: raise ValueError('Signals can not be altered') raise ValueError('Argument not known')
[docs] def __iter__(self): """Iterate over object, used for dict(self).""" for key in self.parameters | self.signals: value = self.__getitem__(key) yield key, value
[docs] class AutoComponent(Component): """ Any component that automatically detects the parameters from the c++ class. This type of component is especially for plugins. No special functions are given to this type of component. """ def __init__(self, component_type, model, objects=None): super().__init__(component_type, model, objects) self._is_initialized = False
[docs] def add_object(self, *arg, **kwarg): super().add_object(*arg, **kwarg) if not self._is_initialized: self._is_initialized = True self.parameters = [] self._detect_parameters_type('parameters_double') self._detect_parameters_type('parameters_int') self._detect_parameters_type('parameters_bool') self.signals = [] self._detect_signals_type('export_double_vectors')
def _detect_parameters_type(self, par_type='parameters_double'): n_parameters = self._model._get_int(f'{self.objects[0]}.n_{par_type}') self.parameters += [ self._model._get_str(f'{self.objects[0]}.{par_type}:{i}') for i in range(n_parameters)] def _detect_signals_type(self, par_type='export_double_vectors'): n_signals = self._model._get_int(f'{self.objects[0]}.n_{par_type}') self.signals += [ self._model._get_str(f'{self.objects[0]}.{par_type}:{i}') for i in range(n_signals)]