class AppStartup(Declarative): """A declarative class for defining a workbench app start-up contribution. AppStartup object can be contributed as extensions child to the 'startup' extension point of the 'ecpy.app' plugin. AppStartup object are used to customize the application start up. """ #: The globally unique identifier for the start-up. id = d_(Unicode()) #: The priority determine the order in which AppStartup are called. The #: **lowest** this number the sooner the object will be called. Two #: AppStartup with the same priority are called in the order in which they #: have been discovered. priority = d_(Int(20)) @d_func def run(self, workbench, cmd_args): """Function called during app start-up. Parameters ---------- workbench : Reference to the application workbench. cmd_args : Commandline arguments passed by the user. """ pass
class ExperimentActionBase(Declarative): # Name of event that triggers command event = d_(Unicode()) dependencies = List() match = Callable() # Defines order of invocation. Less than 100 invokes before default. Higher # than 100 invokes after default. Note that if concurrent is True, then # order of execution is not guaranteed. weight = d_(Int(50)) # Arguments to pass to command by keyword kwargs = d_(Dict()) def _default_dependencies(self): return get_dependencies(self.event) def _default_match(self): code = compile(self.event, 'dynamic', 'eval') if len(self.dependencies) == 1: return partial(simple_match, self.dependencies[0]) else: return partial(eval, code) def __str__(self): return f'{self.event} (weight={self.weight}; kwargs={self.kwargs})'
class BaseOutput(PSIContribution): name = d_(Str()).tag(metadata=True) label = d_(Str()).tag(metadata=True) target_name = d_(Str()).tag(metadata=True) target = d_(Typed(Declarative).tag(metadata=True), writable=False) channel = Property().tag(metadata=True) engine = Property().tag(metadata=True) def _default_label(self): return self.name def _get_engine(self): if self.channel is None: return None else: return self.channel.engine def _get_channel(self): from .channel import Channel target = self.target while True: if target is None: return None elif isinstance(target, Channel): return target else: target = target.target def is_ready(self): raise NotImplementedError
class BaseSettings(GroupBox): """Base widget for creating settings. """ #: Id of this settings (different from the declaration one as multiple #: settings of the same type can exist for a single instrument). user_id = d_(Unicode()) #: Reference to the declaration that created this object. declaration = d_(ForwardTyped(lambda: Settings)) #: Whether or not to make the settings editable read_only = d_(Bool()) @d_func def gather_infos(self): """Return the current values as a dictionary. The base funcion should always be called (using BaseSettings.gather_infos as super is not allowed in declarative functions) and all values should be strings. """ return {'id': self.declaration.id, 'user_id': self.user_id} def _default_title(self): return self.user_id + ' (' + self.declaration.id + ')' def _post_setattr_user_id(self, old, new): if self.declaration: self.title = new + ' (' + self.declaration.id + ')'
class Block(Declarative): name = d_(Unicode()) label = d_(Unicode()) compact_label = d_(Unicode()) factory = d_(Callable()) context_name_map = Typed(dict) blocks = Property() parameters = Property() hide = d_(List()) def initialize(self): super().initialize() for p in self.parameters: if p.name in self.hide: p.visible = False def get_children(self, child_type): return [c for c in self.children if isinstance(c, child_type)] def _get_blocks(self): return self.get_children(Block) def _get_parameters(self): return self.get_children(Parameter)
class Edges(EventInput): initial_state = d_(Int(0)).tag(metadata=True) debounce = d_(Int()).tag(metadata=True) def configure_callback(self): cb = super().configure_callback() return pipeline.edges(self.initial_state, self.debounce, self.fs, cb).send
class Coroutine(Input): coroutine = d_(Callable()) args = d_(Tuple()) force_active = set_default(True) def configure_callback(self): cb = super().configure_callback() return self.coroutine(*self.args, cb).send
class Compare(Declarative): data = Typed(pd.DataFrame) x_column = d_(Unicode()) y_column = d_(Unicode()) as_difference = d_(Bool(True)) jitter = d_(Bool(True)) axes = Typed(pl.Axes) figure = Typed(pl.Figure) selected = Typed(list) def _default_figure(self): return pl.Figure() def _default_axes(self): return self.figure.add_subplot(111) def _observe_data(self, event): self._update_plot() def _observe_x_column(self, event): self._update_plot() def _observe_y_column(self, event): self._update_plot() def _observe_as_difference(self, event): self._update_plot() def _observe_jitter(self, event): self._update_plot() def _default_x_column(self): return self.data.columns[0] def _default_y_column(self): i = 1 if (len(self.data.columns) > 1) else 0 return self.data.columns[i] def _update_plot(self): x = self.data[self.x_column].copy() y = self.data[self.y_column].copy() if self.as_difference: y -= x if self.jitter: x += np.random.uniform(-1, 1, len(x)) y += np.random.uniform(-1, 1, len(x)) self.axes.clear() self.axes.plot(x, y, 'ko', picker=4, mec='w', mew=1) if self.figure.canvas is not None: self.figure.canvas.draw() def pick_handler(self, event): rows = self.data.iloc[event.ind] files = list(rows.index.get_level_values('raw_file')) frequencies = list(rows.index.get_level_values('frequency')) self.selected = list(zip(files, frequencies))
class ManufacturerAlias(Declarative): """Declares that a manufacturer may be known under different names. """ #: Main name under which the vendor is expected to be known id = d_(Unicode()) #: List of aliased names. aliases = d_(List())
class BaseMonitorItem(DockItem): """Base class for the view associated with a monitor. """ #: Reference to the monitor driving this view. This is susceptible to #: change during the lifetime of the widget. monitor = d_(Typed(BaseMonitor)) #: Should this item be made floating by default. float_default = d_(Bool())
class PlotContainer(BasePlotContainer): x_min = d_(Float(0)) x_max = d_(Float(0)) @observe('x_min', 'x_max') def format_container(self, event=None): # If we want to specify values relative to a psi context variable, we # cannot do it when initializing the plots. if (self.x_min != 0) or (self.x_max != 0): self.base_viewbox.setXRange(self.x_min, self.x_max, padding=0)
class NIDAQHardwareAIChannel(NIDAQGeneralMixin, NIDAQTimingMixin, HardwareAIChannel): #: Available terminal modes. Not all terminal modes may be supported by a #: particular device TERMINAL_MODES = 'pseudodifferential', 'differential', 'RSE', 'NRSE' terminal_mode = d_(Enum(*TERMINAL_MODES)).tag(metadata=True) #: Terminal coupling to use. Not all terminal couplings may be supported by #: a particular device. Can be `None`, `'AC'`, `'DC'` or `'ground'`. terminal_coupling = d_(Enum(None, 'AC', 'DC', 'ground')).tag(metadata=True)
class ContextGroup(Declarative): ''' Used to group together context items for management. ''' # Group name name = d_(Unicode()) # Label to use in the GUI label = d_(Unicode()) # Are the parameters in this group visible? visible = d_(Bool(True))
class BaseTimeseriesPlot(SinglePlot): rect_center = d_(Float(0.5)) rect_height = d_(Float(1)) fill_color = d_(Typed(object)) brush = Typed(object) _rising = Typed(list, ()) _falling = Typed(list, ()) def _default_brush(self): return pg.mkBrush(self.fill_color) def _default_plot(self): plot = pg.QtGui.QGraphicsPathItem() plot.setPen(self.pen) plot.setBrush(self.brush) return plot def update(self, event=None): lb, ub = self.parent.data_range.current_range current_time = self.parent.data_range.current_time starts = self._rising ends = self._falling if len(starts) == 0 and len(ends) == 1: starts = [0] elif len(starts) == 1 and len(ends) == 0: ends = [current_time] elif len(starts) > 0 and len(ends) > 0: if starts[0] > ends[0]: starts = np.r_[0, starts] if starts[-1] > ends[-1]: ends = np.r_[ends, current_time] try: epochs = np.c_[starts, ends] except ValueError as e: log.exception(e) log.warning('Unable to update %r, starts shape %r, ends shape %r', self, starts, ends) return m = ((epochs >= lb) & (epochs < ub)) | np.isnan(epochs) epochs = epochs[m.any(axis=-1)] path = pg.QtGui.QPainterPath() y_start = self.rect_center - self.rect_height * 0.5 for x_start, x_end in epochs: x_width = x_end - x_start r = pg.QtCore.QRectF(x_start, y_start, x_width, self.rect_height) path.addRect(r) deferred_call(self.plot.setPath, path)
class Plot(Declarative): #: Id of the plot identifying the contributed plot. id = d_(Str()) #: Description of the kind of plot being declared description = d_(Str()) @d_func def get_cls(self) -> BasePlot: """Access the class implementing the logic.""" pass
class Editor(Declarative): """A declarative class for contributing a measurement editor. Editor object can be contributed as extensions child to the 'editors' extension point of the 'exopy.measurement' plugin. """ #: Unique name used to identify the editor. #: The usual format is top_level_package_name.tool_name id = d_(Str()) #: Editor description. description = d_(Str()) #: Rank of this editor. Editors are displayed by rank and alphabetical #: order rank = d_(Int(100)) @d_func def new(self, workbench, default=True): """Create a new instance of the editor. Parameters ---------- workbench : Workbench Reference to the application workbench. default : bool Whether to use default parameters or not when creating the object. """ raise NotImplementedError() @d_func def is_meant_for(self, workbench, selected_task): """Determine if the editor is fit to be used for the selected task. Parameters ---------- workbench : Workbench Reference to the application workbench. selected_task : BaseTask Currently selected task. Returns ------- answer : bool """ raise NotImplementedError()
class Preferences(Declarative): """Declarative class for defining a workbench preference contribution. Preferences object can be contributed as extensions child to the 'plugin' extension point of a preference plugin. """ #: Id of the contribution. This MUST match the declaring plugin. # FIXME make this self-generated or something id = d_(Str()) #: Short description of what is expected to be saved. description = d_(Str()) #: Name of the method of the plugin contributing this extension to call #: when the preference plugin need to save the preferences. saving_method = d_(Str("preferences_from_members")) #: Name of the method of the plugin contributing this extension to call #: when the preference plugin need to load preferences. loading_method = d_(Str("update_members_from_preferences")) #: The list of plugin members whose values should be observed and whose #: update should cause and automatic update of the preferences. auto_save = d_(List()) #: A callable taking the plugin_id and the preference declaration as arg #: and returning an autonomous enaml view (Container) used to edit #: the preferences. @d_func def edit_view(self, workbench: Workbench, id: str) -> Container: """Create a view to edit the preferences. Parameters ---------- workbench : Reference to the application workbench. id : str Id of the plugin for which to generate the view. Returns ------- view : enaml.widgets.api.Container View used to edit the preferences. It should have a model attribute. The model members must correspond to the tagged members the plugin, their values will be used to update the preferences. """ pass
class FFTContainer(BasePlotContainer): ''' Contains one or more viewboxes that share the same frequency-based X-axis ''' freq_lb = d_(Float(500)) freq_ub = d_(Float(50000)) octave_spacing = d_(Bool(True)) def _default_x_transform(self): return np.log10 @observe('container', 'freq_lb', 'freq_ub') def _update_x_limits(self, event): if not self.is_initialized: # This addresses a segfault that occurs when attempting to load # experiment manifests that use FFTContainer. If the Experiment # manifest attempts to set freq_lb or freq_ub, then it will attempt # to initialize everything else before the GUI is created, leading # to a segfault (creating an AxisItem leads to attempting to call # QGraphicsLabel.setHtml, which will segfault if there is no # instance of QtApplcation). By ensuring we don't continue if the # object is not initialized yet, we can properly load experiment # manifests (e.g., so that `psi` can properly list the available # paradigms). return self.base_viewbox.setXRange(np.log10(self.freq_lb), np.log10(self.freq_ub), padding=0) if self.octave_spacing: major_ticks = util.octave_space(self.freq_lb / 1e3, self.freq_ub / 1e3, 1.0) major_ticklabs = [str(t) for t in major_ticks] major_ticklocs = np.log10(major_ticks * 1e3) minor_ticks = util.octave_space(self.freq_lb / 1e3, self.freq_ub / 1e3, 0.125) minor_ticklabs = [str(t) for t in minor_ticks] minor_ticklocs = np.log10(minor_ticks * 1e3) ticks = [ list(zip(major_ticklocs, major_ticklabs)), list(zip(minor_ticklocs, minor_ticklabs)), ] self.x_axis.setTicks(ticks) else: self.x_axis.setTicks() def _default_x_axis(self): x_axis = super()._default_x_axis() x_axis.setLabel('Frequency', units='Hz') x_axis.logTickStrings = format_log_ticks x_axis.setLogMode(True) return x_axis
class ToneCalibrate(PSIContribution): outputs = d_(Dict()) input_name = d_(Unicode()) gain = d_(Float(-40)) duration = d_(Float(100e3)) iti = d_(Float(0)) trim = d_(Float(10e-3)) max_thd = d_(Value(None)) min_snr = d_(Value(None)) selector_name = d_(Unicode('default')) show_widget = d_(Bool(True)) result = Value()
class Accumulate(ContinuousInput): ''' Chunk data based on number of calls ''' n = d_(Int()).tag(metadata=True) axis = d_(Int(-1)).tag(metadata=True) newaxis = d_(Bool(False)).tag(metadata=True) status_cb = d_(Callable(lambda x: None)) def configure_callback(self): cb = super().configure_callback() return pipeline.accumulate(self.n, self.axis, self.newaxis, self.status_cb, cb).send
class Console(Container): """ Console widget """ proxy = Typed(ProxyConsole) #: Font family, leave blank for default font_family = d_(Unicode()) #: Font size, leave 0 for default font_size = d_(Int(0)) #: Default console size in characters console_size = d_(Coerced(Size,(81,25))) #: Buffer size, leave 0 for default buffer_size = d_(Int(0)) #: Display banner like version, etc.. display_banner = d_(Bool(False)) #: Code completion type #: Only can be set ONCE completion = d_(Enum('ncurses','plain', 'droplist')) #: Run the line or callabla execute = d_(Instance(object)) #: Push variables to the console #: Note this is WRITE ONLY scope = d_(Dict(),readable=False) @observe('font_family','font_size','console_size','buffer_size', 'scope','display_banner','execute','completion') def _update_proxy(self, change): super(Console, self)._update_proxy(change)
class NIDAQTimingMixin(Declarative): #: Specifies sampling clock for the channel. Even if specifying a sample #: clock, you still need to explicitly set the fs attribute. sample_clock = d_(Str().tag(metadata=True)) #: Specifies the start trigger for the channel. If None, sampling begins #: when task is started. start_trigger = d_(Str().tag(metadata=True)) #: Reference clock for the channel. If you aren't sure, a good value is #: `PXI_Clk10` if using a PXI chassis. This ensures that the sample clocks #: across all NI cards in the PXI chassis are synchronized. reference_clock = d_(Str()).tag(metadata=True)
class Channel(PSIContribution): #: Globally-unique name of channel used for identification name = d_(Unicode()).tag(metadata=True) #: Label of channel used in GUI label = d_(Unicode()).tag(metadata=True) #: Is channel active during experiment? active = Property() # SI unit (e.g., V) unit = d_(Unicode()).tag(metadata=True) # Number of samples to acquire before task ends. Typically will be set to # 0 to indicate continuous acquisition. samples = d_(Int(0)).tag(metadata=True) # Used to properly configure data storage. dtype = d_(Unicode()).tag(metadata=True) # Parent engine (automatically derived by Enaml hierarchy) engine = Property() # Calibration of channel calibration = d_(Typed(Calibration, factory=UnityCalibration)) calibration.tag(metadata=True) # Can the user modify the channel calibration? calibration_user_editable = d_(Bool(False)) # Is channel active during experiment? active = Property() filter_delay = d_(Float(0).tag(metadata=True)) def _default_calibration(self): return UnityCalibration() def __init__(self, *args, **kwargs): # This is a hack due to the fact that name is defined as a Declarative # member and each Mixin will overwrite whether or not the name is # tagged. super().__init__(*args, **kwargs) self.members()['name'].tag(metadata=True) def _get_engine(self): return self.parent def _set_engine(self, engine): self.set_parent(engine) def configure(self): pass def _get_active(self): raise NotImplementedError def __str__(self): return self.label
class BasePlot(PSIContribution): # Make this weak-referenceable so we can bind methods to Qt slots. __slots__ = '__weakref__' source_name = d_(Str()) source = Typed(object) label = d_(Str()) def update(self, event=None): pass def _reset_plots(self): pass
class BaseEditor(DestroyablePage): """Base class for all editors. """ #: Declaration defining this editor. declaration = ForwardTyped(lambda: Editor) #: Currently selected task in the tree. selected_task = d_(Typed(BaseTask)) #: Should the tree be visible when this editor is selected. tree_visible = d_(Bool(True)) #: Should the tree be enabled when this editor is selected. tree_enabled = d_(Bool(True)) @d_func def react_to_selection(self, workbench): """Take any necessary actions when the editor is selected. This method is called by the framework at the appropriate time. Parameters ---------- workbench : Workbench Reference to the application workbench. """ pass @d_func def react_to_unselection(self, workbench): """Take any necessary actions when the editor is unselected. This method is called by the framework at the appropriate time. Parameters ---------- workbench : Workbench Reference to the application workbench. """ pass def _default_name(self): """Set the name of the widget otherwise the notebook does not work well """ return self.declaration.id
class ErrorHandler(Declarative): """Handler taking care of certain kind of errors.""" #: Id of the error. When signaling errors it will referred to as the kind. id = d_(Str()) #: Short description of what this handler can do. The keyword for the #: handle method should be specified. description = d_(Str()) @d_func def handle(self, workbench: Workbench, infos: Union[Mapping, List[Mapping]]) -> Widget: """Handle the report by taking any appropriate measurement. The error should always be logged to be sure that a trace remains. Parameters ---------- workbench : Reference to the application workbench. infos : dict or list Information about the error to handle. Should also accept a list of such description. The format of the infos should be described in the description member. Returns ------- widget : enaml.widgets.api.Widget Enaml widget to display as appropriate in a dialog. """ raise NotImplementedError() @d_func def report(self, workbench: Workbench) -> Widget: """Provide a report about all errors that occurred. Implementing this method is optional. Returns ------- widget : enaml.widgets.api.Widget A widget describing the errors that will be included in a dialog by the plugin. If None is returned the report is simply ignored. """ pass
class PSIContribution(Declarative): #: Name of contribution. Should be unique among all contributions for that #: subclass as it is typically used by the plugins to find a particular #: contribution. name = d_(Str()) #: Label of contribution. Not required, but a good idea as it affects how #: the contribution may be represented in the user interface. label = d_(Str()) #: Flag indicating whether item has already been registered. registered = Bool(False) def _default_name(self): # Provide a default name if none is specified TODO: make this mandatory # (i.e., no default?) return self.parent.name + '.' + self.__class__.__name__ def _default_label(self): return self.name.replace('_', ' ') @classmethod def valid_name(self, label): ''' Can be used to convert a label provided to a valid name ''' # TODO: Get rid of this? return re.sub('\W|^(?=\d)', '_', label) def find_manifest_class(self): return find_manifest_class(self) def load_manifest(self, workbench): if self.registered: return try: manifest_class = self.find_manifest_class() manifest = manifest_class(contribution=self) workbench.register(manifest) self.registered = True m = 'Loaded manifest for contribution %s (%s) with ID %r' log.debug(m, self.name, manifest_class.__name__, manifest.id) except ManifestNotFoundError: m = 'No manifest defined for contribution %s' log.warn(m, self.name) except ValueError as e: m = f'Manifest "{manifest.id}" for plugin "{self.name}" already registered.' raise ImportError(m) from e
class Trigger(DigitalOutput): #: Total number of triggers. total_fired = d_(Int(0), writable=False) #: Specifies the default duration for the trigger. This can be overridden #: by specifying a duration when calling `fire`. duration = d_(Float(0.1)) def fire(self, duration=None): if duration is None: duration = self.duration if self.engine.configured: self.engine.fire_sw_do(self.channel.name, duration=duration) self.total_fired += 1
class Starter(Declarative): """Object responsible initializind/finalizing a driver of a certain type. """ #: Unique id identifying this starter. #: The usual format is top_level_package_name.starter_name id = d_(Unicode()) #: Description of the starter action. description = d_(Unicode()) #: Starter instance to use for managing associate instruments. #: Note that the class must be defined in a python file not enaml file #: to be pickeable. starter = d_(Typed(BaseStarter))
class ExperimentActionBase(Declarative): # Name of event that triggers command event = d_(Str()) dependencies = List() match = Callable() #: Defines order of invocation. Less than 100 invokes before default. Higher #: than 100 invokes after default. Note that if concurrent is True, then #: order of execution is not guaranteed. weight = d_(Int(50)) #: Arguments to pass to command by keyword kwargs = d_(Dict()) #: Should action be delayed? If nonzero, this may cause some timing issues. #: Use with caution. delay = d_(Float(0)) def _get_params(self, **kwargs): kwargs.update(self.kwargs) params = {} for k, v in kwargs.items(): if getattr(v, 'is_lookup', False): v = v() params[k] = v return params def _default_dependencies(self): return get_dependencies(self.event) def _default_match(self): code = compile(self.event, 'dynamic', 'eval') if len(self.dependencies) == 1: return partial(simple_match, self.dependencies[0]) else: return partial(eval_match, code) def __str__(self): return f'{self.event} (weight={self.weight}; kwargs={self.kwargs})' def invoke(self, core, **kwargs): if self.delay != 0: timed_call(self.delay * 1e3, self._invoke, core, **kwargs) else: return self._invoke(core, **kwargs)