Source code for finesse.frequency

"""Frequency analysis tools."""

import numpy as np

from finesse.element import ModelElement
from finesse.parameter import float_parameter
from finesse.symbols import Symbol
from finesse.components.general import unique_element

from libc.stdlib cimport free, calloc

[docs]cdef class FrequencyContainer: """Contains an array of frequency objects and their associated faster access C struct information.""" def __cinit__(self, *args, **kwargs): self.size = 0 self.frequency_info = NULL self.carrier_frequency_info = NULL def __init__(self, frequencies, FrequencyContainer carrier_cnt=None): self.size = len(frequencies) self.frequencies = tuple(frequencies)
if self.frequency_info != NULL: raise MemoryError() self.frequency_info = <frequency_info_t*> calloc(self.size, sizeof(frequency_info_t)) if not self.frequency_info: raise MemoryError() if carrier_cnt: if not carrier_cnt.frequency_info: raise MemoryError() self.carrier_frequency_info = carrier_cnt.frequency_info else: self.carrier_frequency_info = NULL def __dealloc__(self): if self.size: free(self.frequency_info) def get_info(self, Py_ssize_t index): if not (0 <= index <= self.size): raise IndexError() rtn = { "f" : self.frequency_info[index].f, "index" : self.frequency_info[index].index, "audio_lower_index" : self.frequency_info[index].audio_lower_index, "audio_upper_index" : self.frequency_info[index].audio_upper_index, "audio_order" : self.frequency_info[index].audio_order, "audio_carrier_index" : self.frequency_info[index].audio_carrier_index, } return rtn cdef initialise_frequency_info(self) : cdef Py_ssize_t i, cidx for i in range(self.size): self.frequency_info[i].f = <double>self.frequencies[i].f self.frequency_info[i].index = <Py_ssize_t>self.frequencies[i].index self.frequency_info[i].audio_order = <int>self.frequencies[i].audio_order if self.frequency_info[i].audio_order: assert(self.carrier_frequency_info) cidx = <Py_ssize_t>self.frequencies[i].audio_carrier_index self.frequency_info[i].audio_carrier_index = cidx self.frequency_info[i].f_car = &self.carrier_frequency_info[cidx].f # Update carrier info so it knows about this sideband if self.frequency_info[i].audio_order == 1: self.carrier_frequency_info[cidx].audio_upper_index = <Py_ssize_t>self.frequencies[i].index elif self.frequency_info[i].audio_order == -1: self.carrier_frequency_info[cidx].audio_lower_index = <Py_ssize_t>self.frequencies[i].index else: raise Exception("Unexpected") # if self.is_audio: # for i in range(len(self.unique_fcnt)): # fcnt = self.unique_fcnt[i] # for j in range(fcnt.size): # fcnt.frequency_info[j].f = <double>fcnt.frequencies[j].f # fcnt.frequency_info[j].index = <Py_ssize_t>fcnt.frequencies[j].index cdef update_frequency_info(self) : """Updates the values of all frequencies in the c-type frequency_info struct.""" cdef Py_ssize_t i for i in range(self.size): self.frequency_info[i].f = <double>self.frequencies[i].f def get_frequency_index(self, value): """For a given value (either float or symbolic) return the index of the frequency with the same value. Parameters ---------- value : [number | symbolic] Frequency value to test for Returns ------- index : int Index for this frequency container """ try: frequency = float(value) except TypeError: frequency = float(value.value) # find the right frequency index for freq in self.frequencies: if freq.f == frequency: f_idx = freq.index break if f_idx is None: raise RuntimeError( f"Could not find a frequency with a value of {frequency} Hz ({value!r})" ) return f_idx def generate_frequency_list(model): """For a given model a symbolic list of frequencies is generated. The result can be used to generate a set of frequencies bins to be modelled in a simulation. This method relies on using :class:`.Symbol`. Using symbolic statements this method attempts to isolate uniqe frequency bins whilst leaving those changing during a simulation present. Returns ------- List of :class:`.Symbol` """ def unique_indices(arr): """Simple unique element finder which doesn't require any greater or less than operations, this isn't tuned for efficiency at all.""" lst = list(arr) lst2 = list(arr) for i in range(len(lst)): if lst.count(lst2[i]) > 1: lst[i] = None return [_ for _, __ in enumerate(lst) if __ is not None] fn_eval = np.vectorize(lambda x: x.eval()) fn_eval2 = np.vectorize(lambda x: x.eval(keep_changing_symbols=True)) # fn_subs = np.vectorize( # lambda x, **kwargs: x.eval(keep_changing_symbols=True, subs=kwargs) # ) fn_is_changing = np.vectorize(lambda x: x.is_changing) source_frequencies = [] source_components = [] modulation_frequencies = [] for comp in model._frequency_generators: s = comp._source_frequencies() source_frequencies.extend(s) source_components.extend((comp,) * len(s)) m = comp._modulation_frequencies() modulation_frequencies.extend(m) # Now to prune the frequency list Nm = len(modulation_frequencies) Ns = len(source_frequencies) if Ns == 0: raise Exception("There are no source frequencies present in the model") if Nm == 0: Fsym = np.array(source_frequencies) else: # First we make a list with all possible combinations of frequencies Fsym = ( np.vstack((np.atleast_2d(modulation_frequencies),) * Ns) + np.hstack((np.atleast_2d(source_frequencies).T,) * Nm) ).flatten() Fsym = np.hstack((source_frequencies, Fsym)) # Take all the frequency values which definitely won't be changing not_changing = Fsym[np.bitwise_not(fn_is_changing(Fsym))] if len(not_changing) == 0: not_changing = [] else: # ... and select all those which are unique _, idx, _, _ = np.unique(fn_eval(not_changing), True, True, True) not_changing = not_changing[idx] # First select only the changing frequency bins changing = Fsym[fn_is_changing(Fsym)] if len(changing) == 0: changing = [] else: # We need to use a different unique finding function that doesn't rely on # using > or < than comparisons like np.unique. I'm not sure how to implement # > and < for Symbols sensibly idx = unique_indices(fn_eval2(changing)) changing = changing[idx] # Finally sort the indicies so that upper and lower sidebands are grouped together. This # help with numerical errors when iterating outputs later so that similarly sized elements are # grouped this isn't always true though, just generally in regards to upper and lower sidebands. final = np.hstack((changing, not_changing)) srt_idx = np.argsort(abs(fn_eval(final))) return final[srt_idx] @unique_element() # only one fsig per model @float_parameter("f", "Signal frequency", units="Hz", validate="_validate_fsig", is_default=True) class Fsig(ModelElement): """This element represents the signal frequency (``fsig``) used in a model. It is a unique element, which means only one can be added to any given model. This is done automatically with the name ``fsig``. It has a single parameter ``f`` for the frequency of the signal. The signal frequency must be set by the user to enable transfer functions and noise projections to be simulated. Parameters ---------- name : str Name of this element f : [float|None] Signal frequency to use in a model [Hz]. If set to ``None`` then no signal frequencies will be modelled in the simulation. """ def __init__(self, name, value): super().__init__(name) self.f = value def _validate_fsig(self, value): if value is None or isinstance(value, Symbol): return value elif value <= 0: raise Exception("fsig value must be > 0 Hz") else: return value
[docs]cdef class Frequency: """Represents a frequency "bin" with a specific index. The value of the frequency is calculated from the name of the frequency. Parameters ---------- name : str Name of the frequency. order : int, optional
The order of the frequency, defaults to zero. """ def __init__( self, name, symbol, *, index=None, audio=False, audio_carrier_index=None, audio_carrier_object=None, audio_order=0, ): self.__name = name self.__symbol = symbol self.__index = index self.__is_audio = audio self.__symbol_changing = symbol.is_changing self.__start_value = self.__symbol.eval() self.__lambdified = self.__symbol.lambdify() if audio: if audio_carrier_index is None: raise Exception("Audio frequency carrier must be specified") if audio_order not in (-1, 1): raise Exception("Audio frequency order must be -1 or +1") self.__order = audio_order self.__carrier = audio_carrier_index self.__carrier_obj = audio_carrier_object else: self.__order = 0 self.__carrier = 0 def __repr__(self): return ( f"<{self.__class__.__name__} {self.name} (f={self.symbol}={self.f}) at " f"{ hex(id(self)) }>" ) def __str__(self): return f"<{self.name} is {self.f}Hz (Frequency)>" def __deepcopy__(self, memo): raise Exception( "Frequency objects cannot be deepcopied as they are associated with Simulations" ) @property def symbol(self): return self.__symbol @property def f(self): if self.__symbol_changing: return self.__lambdified() else: return self.__start_value @property def is_audio(self): """Is this an audio sideband frequency?""" return self.__is_audio @property def audio_carrier_index(self): """The carrier frequency. :`getter`: Returns the carrier frequency index (read-only). """ return self.__carrier @property def name(self): """Name of the frequency object. :`getter`: Returns the name of the frequency (read-only). """ return self.__name @property def audio_order(self): """Audio modulation order of this frequency. :`getter`: Returns the order of the frequency (read-only). """ return self.__order @property def index(self): """Index of the frequency object. :`getter`: Returns the index of the frequency (read-only). """ return self.__index @property def audio_carrier(self): """Frequency object for the carrier frequency of this sideband, if it is a sideband. :`getter`: Returns the index of the frequency (read-only). """ if self.__is_audio: return self.__carrier_obj else: return None