"""
Modules are the basic building blocks of Pyrpl.
The internal structure of the FPGA is made of individual modules, each
performing a well defined task. Each of these FPGA modules are represented
in python by a :obj:`HardwareModule`.
Higher-level operations, for instance those that need a coordinated
operation of several HardwareModules is performed by a SoftwareModule,
defined in a class derived from :obj:`Module`.
Thus, all modules (both :obj:`HardwareModule` and Software modules inherit
from :obj:`Module` which gives them basic capabilities such as displaying
their attributes in the GUI having their state load and saved in the config
file.
"""
from .attributes import BaseAttribute, ModuleAttribute
from .widgets.module_widgets import ModuleWidget
from .curvedb import CurveDB
from .pyrpl_utils import unique_list, DuplicateFilter
from .errors import ExpectedPyrplError, UnexpectedPyrplError
import logging
import numpy as np
from six import with_metaclass
from collections import OrderedDict
from qtpy import QtCore
[docs]class SignalLauncher(QtCore.QObject):
"""
Object that is used to handle signal the emission for a :obj:`Module`.
A QObject that is connected to the widgets to update their value when
attributes of a module change. Any timers needed to implement the module
functionality shoud be implemented here as well.
"""
update_attribute_by_name = QtCore.Signal(str, list)
# The name of the property that has changed, the list is [new_value],
# the new_value of the attribute
change_options = QtCore.Signal(str, list) # name of the
# SelectProperty, list of new options
refresh_filter_options = QtCore.Signal(str) # name of the
# FilterProperty, new options are contained in self.valid_frequencies()
change_ownership = QtCore.Signal() # The owner of the module has
# changed
def __init__(self, module):
super(SignalLauncher, self).__init__()
self.module = module
[docs] def emit_signal_by_name(self, name, *args, **kwds):
"""Emits signal "name" with the specfified args and kwds."""
signal = getattr(self, name)
signal.emit(*args, **kwds)
def _clear(self):
""" Destroys the object by disconnecting all signals and by killing all timers"""
for key in dir(self.__class__):
val = getattr(self, key)
if isinstance(val, QtCore.pyqtBoundSignal):
try:
val.disconnect()
except TypeError: # occurs if signal is not connected to anything
pass
[docs]class DoSetup(object):
"""
A context manager that allows to nicely write Module setup functions.
Usage example in :py:meth:`Module._setup()`::
def _setup(self):
# _setup_ongoing is False by default
assert self._setup_ongoing == False
with self.do_setup:
# now _setup_ongoing is True
assert self._setup_ongoing == True
# do stuff that might fail
raise BaseException()
# even if _setup fails, _setup_ongoing is False afterwards or in
# the next call to _setup()
assert self._setup_ongoing == False
"""
def __init__(self, parent):
self.parent = parent
def __enter__(self):
self.parent._setup_ongoing = True
def __exit__(self, exc_type, exc_val, exc_tb):
self.parent._setup_ongoing = False
if exc_type is not None:
self.parent._logger.warning("Exception %s was raised while "
"_setup_ongoing was True: %s, %s",
exc_type, exc_val, exc_tb)
[docs]class Module(with_metaclass(ModuleMetaClass, object)):
# The Syntax for defining a metaclass changed from Python 2 to 3.
# with_metaclass is compatible with both versions and roughly does this:
# def with_metaclass(meta, *bases):
# """Create a base class with a metaclass."""
# return meta("NewBase", bases, {})
# Specifically, ModuleMetaClass ensures that attributes have automatically
# their internal name set properly upon module creation.
"""
A module is a component of pyrpl doing a specific task.
Module is the base class for instruments such as the
Scope/Lockbox/NetworkAnalyzer. A module can have a widget to build a
graphical user interface on top of it.
It is composed of attributes (see attributes.py) whose values represent
the current state of the module (more precisely, the state is defined
by the value of all attributes in _setup_attributes)
The module can be slaved or freed by a user or another module. When the
module is freed, it goes back to the state immediately before being
slaved. To make sure the module is freed, use the syntax::
with pyrpl.mod_mag.pop('owner') as mod:
mod.do_something()
mod.do_something_else()
Attributes:
`get_setup_attributes()`: returns a dict with the current values of
the setup attributes
``set_setup_attributes(**kwds)``: sets the provided setup_attributes
(setup is not called)
`save_state(name)`: saves the current 'state' (using
get_setup_attribute) into the config file
`load_state(name)`: loads the state 'name' from the config file (setup
is not called by default)
`erase_state(name)`: erases state 'name' from config file
`create_widget()`: returns a widget according to widget_class
``setup(**kwds)``: first, performs :code:`set_setup_attributes(**kwds)`,
then calls _setup() to set the module ready for acquisition. This
method is automatically created by ModuleMetaClass and it combines the
docstring of individual setup_attributes with the docstring of _setup()
`free()`: sets the module owner to None, and brings the module back the
state before it was slaved equivalent to module.owner = None)
`get_yml(state=None)`: get the yml code representing the state "state'
or the current state if state is None
`set_yml(yml_content, state=None)`: sets the state "state" with the
content of yml_content. If state is None, the state is directly loaded
into the module.
`name`: attributed based on name at instance creation
(also used as a section key in the config file)
`states (list)`: the list of states available in the config file
`owner (string)`: a module can be owned (reserved) by a user or another
module. The module is free if and only if owner is None
`pyrpl` (:obj:`Pyrpl`): recursively looks through parent modules until it
reaches the Pyrpl instance
Class attributes to be implemented in derived class:
- all individual attributes (instances of BaseAttribute)
- _setup_attributes: attribute names that are touched by setup(**kwds)/
saved/restored upon module creation
- _gui_attributes: attribute names to be displayed by the widget
- _callback_attributes: attribute_names that triggers a callback when
their value is changed in the base class, _callback just calls setup()
- _widget_class: class of the widget to use to represent the module in
the gui(a child of ModuleWidget)
Methods to implement in derived class:
- _setup(): sets the module ready for acquisition/output with the
current attribute's values. The metaclass of the module autogenerates a
function like this::
def setup(self, **kwds):
\"\"\"
_ docstring is the result of the following pseudocode: _
print(DOCSTRING_OF_FUNCTION("_setup"))
for attribute in self.setup_attributes:
print(DOCSTRING_OF_ATTRIBUTE(attribute))
\"\"\"
self.set_setup_attributes(kwds)
return self._setup()
- _ownership_changed(old, new): this function is called when the module
owner changes it can be used to stop the acquisition for instance.
"""
# Change this to provide a custom graphical class
_widget_class = ModuleWidget
# the class for the SignalLauncher to be used
# a QOBject used to communicate with the widget
_signal_launcher = SignalLauncher
# attributes listed here will be saved in the config file everytime they
# are updated.
_setup_attributes = []
# class inheriting from ModuleWidget can
# automatically generate gui from a list of attributes
_gui_attributes = []
# This flag is used to desactivate callback during setup
_setup_ongoing = False
# internal memory for owner of the module (to avoid conflicts)
_owner = None
# name of the module, metaclass automatically assigns one per instance
name = None
def __init__(self, parent, name=None):
"""
Creates a module with given name. If name is None, cls.name is
assigned by the metaclass.
Parent is either
- a pyrpl instance: config file entry is in
(self.__class__.name + 's').(self.name)
- or another SoftwareModule: config file entry is in
(parent_entry).(self.__class__.name + 's').(self.name)
"""
if name is not None:
self.name = name
self.do_setup = DoSetup(self) # ContextManager for _setup_ongoing
self._flag_autosave_active = True # I would have prefered to use
# __autosave_active, but this gets automatically name mangled:
# see http://stackoverflow.com/questions/1301346/what-is-the-meaning-of-a-single-and-a-double-underscore-before-an-object-name
self._logger = logging.getLogger(name=__name__)
self._logger.addFilter(DuplicateFilter())
# create the signal launcher object from its class
self._signal_launcher = self._signal_launcher(self)
self.parent = parent
# disable autosave during initialization
self._autosave_active = False
# instantiate modules associated with _module_attribute by calling their getter
for submodule in self._module_attributes:
getattr(self, submodule)
# custom module initialization hook
# self._init_module()
# enable autosave and load last state from config file
self._autosave_active = True
# Only top level modules should call _load_setup_attributes() since
# this call propagates through all child modules
##if not isinstance(self.parent, Module):
## # attributes are loaded but _setup() is not called
## self._load_setup_attributes()
def _init_module(self):
"""
To implement in child class if needed.
"""
self._logger.warning("Function _init_module is obsolete and will be "
"removed soon. Please migrate the corresponding "
"code to __init__.")
@property
def _autosave_active(self):
"""
:return: If an ancestor of the current module is NOT autosaving, then
the current module is not saving either.
"""
try:
parent_autosave_active = self.parent._autosave_active
except AttributeError: # some parents do not implement the autosave flag
parent_autosave_active = True
return self._flag_autosave_active and parent_autosave_active
@_autosave_active.setter
def _autosave_active(self, val):
"""
Only takes effect when all ancestor are autosaving
:param val:
:return:
"""
self._flag_autosave_active = val
@property
def _modules(self):
return dict([(key, getattr(self, key)) for key in
self._module_attributes])
@property
def pyrpl(self):
"""
Recursively looks through patent modules untill pyrpl instance is
reached.
"""
from .pyrpl import Pyrpl
parent = self.parent
while (not isinstance(parent, Pyrpl)):
if parent.parent == parent:
raise AttributeError(
"Module %s.pyrpl does not have a parent of type 'Pyrpl'. "
"Highest parent is %s. " % (self.name, parent))
else:
parent = parent.parent
return parent
[docs] def get_setup_attributes(self):
"""
Returns a dict with the current values of the setup attributes.
Recursively calls get_setup_attributes for sub_modules and assembles
a hierarchical dictionary.
Returns:
dict: contains setup_attributes and their current values.
"""
self._logger.warning("get_setup_attributes is deprecated. Use property setup_attributes instead. ")
return self.setup_attributes
@property
def setup_attributes(self):
"""
:return: a dict with the current values of the setup attributes.
Recursively collects setup_attributes for sub_modules.
"""
kwds = OrderedDict()
for attr in self._setup_attributes:
val = getattr(self, attr)
if attr in self._modules:
val = val.setup_attributes
kwds[attr] = val
return kwds
[docs] def set_setup_attributes(self, **kwds):
"""
Sets the values of the setup attributes. Without calling any callbacks
"""
self._logger.warning("set_setup_attributes is deprecated. Use property setup_attributes instead. ")
self.setup_attributes = kwds
@setup_attributes.setter
def setup_attributes(self, kwds):
"""
Sets the values of the setup attributes.
"""
self.setup(**kwds)
def _load_setup_attributes(self):
"""
Load and sets all setup attributes from config file
"""
# self.c = None switches off loading states (e.g. for ModuleManagers).
# First part of the if avoids creating an empty branch in the
# config file at the call of this function at startup.
if (self.name in self.parent.c) and (self.c is not None):
# pick those elements of the config state that are setup_attributes
dic = {k: v for k, v in self.c._data.items() if k in self._setup_attributes}
# set those elements
self.setup_attributes = dic
@property
def c(self):
"""
Returns a MemoryBranch object used for storing data in the configuration file.
The branch corresponding to the module is a subbranch of the parent module's branch with the name of the module.
"""
return self.parent.c._get_or_create(self.name)
@property
def _states(self):
"""
Returns the config file branch corresponding to the saved states of the module.
"""
return self.c._root._get_or_create(self.name + "_states")
@property
def states(self):
"""
Returns the names of all saved states of the module.
"""
# the if avoids creating an empty states section for all modules
if (self.name + "_states") in self.parent.c._root._data:
return list(self._states._keys())
else:
return []
[docs] def save_state(self, name=None):
"""
Saves the current state under the name "name" in the config file. If
state_section is left unchanged, uses the normal
class_section.states convention.
"""
if name is None:
self.setup_attributes = self.setup_attributes
else:
self._states[name] = self.setup_attributes
[docs] def load_state(self, name=None):
"""
Loads the state with name "name" from the config file. If
state_branch is left unchanged, uses the normal
class_section.states convention.
"""
if name is None:
self.setup_attributes = self.c._data
else:
self.setup_attributes = self._states[name]._data
[docs] def erase_state(self, name):
"""
Removes the state "name' from the config file
:param name: name of the state to erase
:return: None
"""
self._states[name]._erase()
[docs] def get_yml(self, state=None):
"""
:param state: The name of the state to inspect. If state is None-->
then, use the current instrument state.
:return: a string containing the yml code
"""
if state is None:
return self.c._get_yml()
else:
return self._states[state]._get_yml()
[docs] def set_yml(self, yml_content, state=None):
"""
:param yml_content: some yml code to encode the module content.
:param state: The name of the state to set. If state is None-->
then, use the current instrument state and reloads it immediately
:return: None
"""
if state is None:
self.c._set_yml(yml_content)
self._load_setup_attributes()
else:
self._states._get_or_create(state)._set_yml(yml_content)
def _save_curve(self, x_values, y_values, **attributes):
"""
Saves a curve in some database system.
To change the database system, overwrite this function
or patch Module.curvedb if the interface is identical.
:param x_values: numpy array with x values
:param y_values: numpy array with y values
:param attributes: extra curve parameters (such as relevant module
settings)
"""
curve = CurveDB.create(x_values,
y_values,
**attributes)
return curve
[docs] def free(self):
"""
Change ownership to None
"""
self.owner = None
def _setup(self):
"""
Sets the module up for acquisition with the current setup attribute
values.
"""
pass
# def help(self, register=''):
# """returns the docstring of the specified register name
# if register is an empty string, all available docstrings are
# returned"""
# if register:
# string = type(self).__dict__[register].__doc__
# return string
# else:
# string = ""
# for key in type(self).__dict__.keys():
# if isinstance(type(self).__dict__[key], BaseAttribute):
# docstring = self.help(key)
# # mute internal registers
# if not docstring.startswith('_'):
# string += key + ": " + docstring + '\r\n\r\n'
# return string
[docs] def help(self, register=''):
return "Please refer to the docstring of the function setup() or " \
"to the manual for further help! "
def _create_widget(self):
"""
Creates the widget specified in widget_class.
"""
if self._widget_class is None:
self._logger.warning("Module %s of type %s is trying to create a widget, but no widget_class is defined!",
self.name, type(self))
return None
try:
widget = self._widget_class(self.name, self)
finally:
pass
self._module_widget = widget # For debugging purpose only (all
# communications to the widget should happen via signals)
return widget
@property
def owner(self):
return self._owner
@owner.setter
def owner(self, val):
"""
Changing module ownership automagically:
- changes the visibility of the module_widget in the gui
- re-setups the module with the module attributes in the config-file
if new ownership is None
"""
old = self.owner
self._owner = val
if val is None:
self._autosave_active = True
else:
# deactivate autosave for slave modules
self._autosave_active = False
self._ownership_changed(old, val)
if val is None:
self._load_setup_attributes()
# self.set_setup_attributes(**self.c._dict)
# using the same dict will create a reference (&id) in the
# config file for submodules --> That is probably a bug that
# could be solved by making a copy of the dict somewhere in
# memory.py, but on the other hand we are not supposed to use
# anything but the public API of memory.py
self._signal_launcher.change_ownership.emit()
def _ownership_changed(self, old, new):
pass
def __enter__(self):
"""
This function is executed in the context manager construct with
... as ... :
"""
return self
def __exit__(self, type, val, traceback):
"""
To make sure the module will be freed afterwards, use the context
manager construct:
with pyrpl.module_manager.pop('owner') as mod:
mod.do_something()
# module automatically freed at this point
The free operation is performed in this function
see http://stackoverflow.com/questions/1369526/what-is-the-python-keyword-with-used-for
"""
self.owner = None
def _clear(self):
"""
Kill timers and free resources for this module and all submodules.
"""
self._signal_launcher._clear()
for sub in self._modules:
getattr(self, sub)._clear()
[docs]class HardwareModule(Module):
"""
Module that directly maps a FPGA module. In addition to BaseModule's
requirements, HardwareModule classes must have the following class
attributes:
- addr_base (int): the base address of the module, such as 0x40300000
"""
parent = None # parent will be redpitaya instance
def __init__(self, parent, name=None):
""" Creates the prototype of a RedPitaya Module interface
if no name provided, will use cls.name
"""
self._client = parent.client
self._addr_base = self.addr_base
self._rp = parent
super(HardwareModule, self).__init__(parent, name=name)
#self.__doc__ = "Available registers: \r\n\r\n" + self.help()
def _ownership_changed(self, old, new):
"""
This hook is there to make sure any ongoing measurement is stopped when
the module gets slaved
old: name of old owner (eventually None)
new: name of new owner (eventually None)
"""
pass
@property
def _frequency_correction(self):
"""
factor to manually compensate 125 MHz oscillator frequency error
real_frequency = 125 MHz * _frequency_correction
"""
try:
return self._rp.frequency_correction
except AttributeError:
self._logger.warning("Warning: Parent of %s has no attribute "
"'frequency_correction'. ", self.name)
return 1.0
def _reads(self, addr, length):
return self._client.reads(self._addr_base + addr, length)
def _writes(self, addr, values):
self._client.writes(self._addr_base + addr, values)
def _read(self, addr):
return int(self._reads(addr, 1)[0])
def _write(self, addr, value):
self._writes(addr, [int(value)])
def _to_pyint(self, v, bitlength=14):
v = v & (2 ** bitlength - 1)
if v >> (bitlength - 1):
v = v - 2 ** bitlength
return int(v)
def _from_pyint(self, v, bitlength=14):
v = int(v)
if v < 0:
v = v + 2 ** bitlength
v = (v & (2 ** bitlength - 1))
return np.uint32(v)
[docs]class SignalModule(Module):
""" any module that can be passed as an input to another module"""
[docs] def signal(self):
return self.name