class DataView(HasTraits): ''' Show location data as cursor moves about''' # latitude, longitude for current cursor latitude = Float(0) longitude = Float(0) # latitude, longitude for current cursor easting = Float(0) northing = Float(0) # depth of current mouse position depth = Float(0) # power & gain at current cursor power = CFloat(0) gain = CFloat(0) traits_view = View(Item('latitude'), Item('longitude'), Item('_'), Item('easting'), Item('northing'), Item('_'), Item('depth'), Item('_'), Item('power'), Item('gain'), resizable=True)
class SampleHole(HasTraits): id = Str x = Float y = Float x_cor = CFloat(0) y_cor = CFloat(0) render = Str shape = Str dimension = Float interpolated = False corrected = False interpolation_holes = None analyzed = False nominal_position = Property(depends_on='x,y') corrected_position = Property(depends_on='x_cor,y_cor') associated_hole = Str def _get_corrected_position(self): return self.x_cor, self.y_cor def _get_nominal_position(self): return self.x, self.y def has_correction(self): return self.corrected
class AbstractNumericSensor(AbstractSensor): """ Abstract class for numeric sensor types, that allows limiting value within a specific range. If limiting values (:attr:`.value_min`, :attr:`.value_max`) are used, value that exceeds these limits, is clipped to the range. """ #: Minimum allowed value for status value_min = CFloat(float('-inf')) #: Maximum allowed value for status value_max = CFloat(float('inf')) view = AbstractSensor.view + ['value_min', 'value_max'] @property def is_finite_range(self): return self.value_max - self.value_min < float('inf') def get_as_datadict(self): d = super().get_as_datadict() d.update(dict(value_min=self.value_min, value_max=self.value_max)) return d def set_status(self, status, **kwargs): if status is None: clipped_status = None else: clipped_status = max(min(float(status), self.value_max), self.value_min) super().set_status(clipped_status, **kwargs)
class ExportPane(TraitsDockPane): id = 'edu.mit.synbio.cytoflowgui.export_pane' name = 'Export' # the task serving as the dock pane's controller task = Instance(Task) closable = False dock_area = 'right' floatable = False movable = False visible = True # the dinky little model that this pane displays width = CFloat(11) height = CFloat(8.5) dpi = CInt(96) do_exit = Event do_export = Event def default_traits_view(self): return View( Item('width', editor=TextEditor(auto_set=False)), Item('height', editor=TextEditor(auto_set=False)), Item('dpi', editor=TextEditor(auto_set=False)), Item('do_export', editor=ButtonEditor(value=True, label="Export figure..."), show_label=False), Item('do_exit', editor=ButtonEditor(value=True, label="Return to Cytoflow"), show_label=False))
class RangeSubset(HasStrictTraits): name = Str values = List high = CFloat(Undefined) low = CFloat(Undefined) str = Property(Str, depends_on = "name, values, high, low") def default_traits_view(self): return View(Item('high', label = self.name, editor = ValuesBoundsEditor( name = 'values', low_name = 'low', high_name = 'high', format = '%g', auto_set = False))) # MAGIC: gets the value of the Property trait "subset_str" def _get_str(self): if self.low == self.values[0] and self.high == self.values[-1]: return "" elif self.low == self.high: return "({0} == {1})" \ .format(util.sanitize_identifier(self.name), self.low) else: return "({0} >= {1} and {0} <= {2})" \ .format(util.sanitize_identifier(self.name), self.low, self.high) @on_trait_change('values, values[]') def _values_changed(self): if self.high is Undefined: self.high = max(self.values) if self.low is Undefined: self.low = min(self.values) def __eq__(self, other): return (self.name == other.name and self.values == other.values and self.low == other.low and self.high == other.high) def __hash__(self): return hash((self.name, tuple(self.values), self.low, self.high))
class AbstractPollingSensor(AbstractSensor): """ Abstract baseclass for sensor that polls periodically its status""" #: How often to do polling interval = CFloat(5) #: This can be used to enable/disable polling poll_active = CBool(True) _stop = CBool(False, transient=True) _pollthread = Any(transient=True) view = AbstractSensor.view + ["interval"] silent = CBool(True) history_frequency = CFloat(1.0) def setup(self): self._restart() def _poll_active_changed(self, old, new): if not self.traits_inited(): return if new: self._restart() else: self._pollthread.cancel() def _restart(self): if self._stop: return if self._pollthread and self._pollthread.is_alive(): self._pollthread.cancel() if self.poll_active: self.update_status() self._pollthread = threading.Timer( self.interval, threaded(self.system, self._restart)) time_after_interval = datetime.now() + timedelta( seconds=self.interval) self._pollthread.name = "PollingSensor: %s next poll at %s (%.2f sek)" % ( self.name, time_after_interval, self.interval) self._pollthread.start() def update_status(self): pass def cleanup(self): self._stop = True if self._pollthread: self._pollthread.cancel()
class Converter(HasStrictTraits): # Trait definitions: input_amount = CFloat(12.0, desc="the input quantity") input_units = Units('inches', desc="the input quantity's units") output_amount = Property( depends_on=['input_amount', 'input_units', 'output_units'], desc="the output quantity") output_units = Units('feet', desc="the output quantity's units") # User interface views: traits_view = View(VGroup(HGroup(Item('input_amount', springy=True), Item('input_units', show_label=False), label='Input', show_border=True), HGroup(Item('output_amount', style='readonly', springy=True), Item('output_units', show_label=False), label='Output', show_border=True), help=ViewHelp), title='Units Converter', buttons=['Undo', 'OK', 'Help']) # Property implementations def _get_output_amount(self): return ((self.input_amount * self.input_units_) / self.output_units_)
class StatusSaverService(AbstractUserService): """ Service which is responsible for scheduling dumping system into file periodically. """ autoload = True #: Dump saving interval, in seconds. Default 30 minutes. dump_interval = CFloat(30 * 60) _exit = CBool(False) _timer = Any(transient=True) def setup(self): if self.system.filename: self.system.on_trait_change(self.exit_save, "pre_exit_trigger") if self.dump_interval: self.save_system_periodically() def save_system_periodically(self): self.logger.debug('Saving system state') self.system.save_state() self._timer = Timer(self.dump_interval, self.save_system_periodically) self._timer.start() def exit_save(self): self.system.save_state() self._exit = True def cleanup(self): if self._timer and self._timer.is_alive(): self._timer.cancel()
def _get_trait(self): if self.type == 'metadata' or self.type == 'category': return CStr() elif self.type == 'float': return CFloat() elif self.type == 'bool': return ConvertingBool()
class WarpScalarFactory(PipeFactory): """Applies the WarpScalar mayavi filter to the given VTK object.""" _target = Instance(filters.WarpScalar, ()) warp_scale = CFloat(1.0, adapts="filter.scale_factor", help="scale of the warp scalar")
class Rectangle(HasTraits): width = CFloat(1.0) height = CFloat(2.0) area = Property(depends_on=['width', 'height']) view = View(VGroup( HGroup(Item('width', label='W'), Item('height', label='H')), Item('area', style='readonly'), ), buttons=OKCancelButtons) def _get_area(self): return self.width * self.height
class Title(SingletonModuleFactory): """Creates a title for the figure. **Function signature**:: title(text, ...) """ size = CFloat(1, help="the size of the title") height = CFloat(0.8, adapts='y_position', help="""height of the title, in portion of the figure height""") def _size_changed(self): self._target.width = min(0.05 * self.size * len(self._text), 1) self._target.x_position = 0.5 * (1 - self._target.width) _target = Instance(modules.Text) def __target_default(self): """ This is called only if no existing title is found.""" width = min(0.05 * self.size * len(self._text), 1) text = modules.Text( text=self._text, y_position=self.height, x_position=0.5 * (1 - width), ) text.width = width return text def __init__(self, text, **kwargs): self._text = text # This will be used by _size_changed if not 'name' in kwargs: # The name is used as au unique marker to identify the # title. We need to set it ASAP. self.name = kwargs['name'] = 'Title' super(Title, self).__init__(**kwargs) self._target.text = self._text # We need to set position after Text is initiated, as text will # override these positions self._target.y_position = self.height self._size_changed()
class TemperatureSensor(AbstractPollingSensor): """ W1 interface (on Raspberry Pi board) that polls polling temperature. (kernel modules w1-gpio and w1-therm required). Not using RPIO, but placed this here, since this is also Raspberry Pi related sensor. """ _status = CFloat #: Address of W1 temperature sensor (something like ``"28-00000558263c"``), see what you have in #: ``/sys/bus/w1/devices/`` addr = CUnicode view = list(set(UserFloatSensor.view + AbstractPollingSensor.view + ["addr"])) #: Maximum jump in temperature, between measurements. These temperature sensors #: tend to give sometimes erroneous results. max_jump = CFloat(5.0) _error_count = Int(0, transient=True) #: Maximum number of erroneous measurements, until value is really set max_errors = Int(5) _first_reading = CBool(True, transient=True) def get_status_display(self, **kwargs): if 'value' in kwargs: value = kwargs['value'] else: value = self.status return u"%.1f ⁰C" % value def update_status(self): w1file = "/sys/bus/w1/devices/%s/w1_slave" % self.addr try: f = open(w1file) except IOError: self.logger.error("IO-error, can't open %s, not set", w1file) return try: temp = float(f.read().split("\n")[1].split(" ")[9].split("=")[1]) / 1000. except IOError: self.logger.error("IO-error, cant' read %s, not set", w1file) return if (abs(temp-self.status) > self.max_jump and self._error_count < self.max_errors and not self._first_reading): self._error_count += 1 else: self._first_reading = False self._error_count = 0 self.set_status(temp) f.close()
class ModuleFactory(PipeFactory): """ Base class for all the modules factories""" color = Trait( None, None, TraitTuple(Range(0., 1.), Range(0., 1.), Range(0., 1.)), help="""the color of the vtk object. Overides the colormap, if any, when specified. This is specified as a triplet of float ranging from 0 to 1, eg (1, 1, 1) for white.""", ) def _color_changed(self): if self.color: self._target.actor.property.color = self.color if hasattr(self._target.actor.mapper, "scalar_visibility"): self._target.actor.mapper.scalar_visibility = False if hasattr(self._target, "property"): self._target.property.color = self.color opacity = CFloat(1., desc="""The overall opacity of the vtk object.""") def _opacity_changed(self): try: self._target.actor.property.opacity = self.opacity except AttributeError: try: self._target.property.opacity = self.opacity except AttributeError: pass line_width = CFloat(2., desc=""" The width of the lines, if any used.""") def _line_width_changed(self): try: self._target.actor.property.line_width = self.line_width except (AttributeError, TraitError): try: self._target.property.line_width = self.line_width except (AttributeError, TraitError): pass
class TubeFactory(PipeFactory): """Applies the Tube mayavi filter to the given VTK object.""" _target = Instance(filters.Tube, ()) tube_sides = CInt(6, adapts='filter.number_of_sides', desc="""number of sides of the tubes used to represent the lines.""") tube_radius = CFloat(0.05, adapts='filter.radius', desc="""radius of the tubes used to represent the lines.""")
class Text3D(ModuleFactory): """ Positions text at a 3D location in the scene. **Function signature**:: text3d(x, y, z, text, ...) x, y, and z are the position of the origin of the text. The text is positioned in 3D, in figure coordinates. """ _target = Instance(modules.Text3D, ()) scale = Either(CFloat(1), CArray(shape=(3, )), help="""The scale of the text, in figure units. Either a float, or 3-tuple of floats.""") orientation = CArray(shape=(3, ), adapts='orientation', desc="""the angles giving the orientation of the text. If the text is oriented to the camera, these angles are referenced to the axis of the camera. If not, these angles are referenced to the z axis.""") orient_to_camera = Bool(True, adapts='orient_to_camera', desc="""if the text is kept oriented to the camera, or is pointing in a specific direction, regardless of the camera position.""") def __init__(self, x, y, z, text, **kwargs): """ Override init as for different positional arguments.""" if not 'scale' in kwargs: kwargs['scale'] = 1 super(Text3D, self).__init__(None, **kwargs) self._target.text = text self._target.position = (x, y, z) def _scale_changed(self): scale = self.scale if isinstance(scale, numbers.Number): scale = scale * np.ones((3, )) self._target.scale = scale
class RpioSensor(UserBoolSensor): """ Boolean-valued sensor object that reads Raspberry Pi GPIO input pins. """ user_editable = CBool(False) #: GPIO port port = Int #: Set to True to have inversed status value inverted = Bool(False) #: Button setup: "down": pushdown resistor, "up": pushup resistor, or "none": no resistor set up. button_type = Enum("down", "up", "none") view = UserBoolSensor.view + ["port", "button_type"] history_frequency = CFloat(1.0) _hw_service = Instance(AbstractSystemService, transient=True) def setup(self): self._hw_service = self.system.request_service('RpioService') self._hw_service.enable_input_port(self.port, self.gpio_callback, self.button_type) def _button_type_changed(self, new): if self._hw_service: self._hw_service.disable_input_port(self.port) self._hw_service.enable_input_port(self.port, self.gpio_callback, new) def gpio_callback(self, gpio_id, value): self.set_status(value if not self.inverted else not value) def update_status(self): self.gpio_callback(None, self._hw_service.get_input_status(self.port)) def _port_changed(self, old, new): if not self._hw_service: return if old: self._hw_service.disable_input_port(old) self._hw_service.enable_input_port(new, self.gpio_callback, self.button_type)
class Text(ModuleFactory): """ Adds a text on the figure. **Function signature**:: text(x, y, text, ...) x, and y are the position of the origin of the text. If no z keyword argument is given, x and y are the 2D projection of the figure, they belong to [0, 1]. If a z keyword argument is given, the text is positionned in 3D, in figure coordinnates. """ width = Trait(None, None, CFloat, adapts='width', help="""width of the text.""") z = Trait(None, None, CFloat, help="""Optional z position. When specified, the text is positioned in 3D""") _target = Instance(modules.Text, ()) opacity = CFloat(1, adapts="property.opacity", help="""The opacity of the text.""") def __init__(self, x, y, text, **kwargs): """ Override init as for different positional arguments.""" if 'z' in kwargs and kwargs['z'] is not None: self._target.z_position = kwargs['z'] self._target.position_in_3d = True elif not (x < 1. and x > 0. and y > 0. and y < 1.): raise ValueError('Text positions should be in [0, 1] if no z' 'position is given') super(Text, self).__init__(None, **kwargs) self._target.text = text self._target.x_position = x self._target.y_position = y
class AbstractArduinoSensor(AbstractSensor): """ Abstract base class for Arduino sensors """ history_frequency = CFloat(1.0) user_editable = CBool(False) #: Arduino service number (specify, if more than 1 ArduinoServices are configured in system) service = CInt(0) #: Arduino pin number pin = CInt _arduino = Instance(AbstractSystemService, transient=True) def setup(self, *args, **kwargs): super().setup(*args, **kwargs) self.logger.debug('Arduino sensor setup') self._arduino = self.system.request_service('ArduinoService', self.service)
class ArduinoServoActuator(AbstractArduinoActuator): """ Float-valued actuator object for Arduino output pins that can be configured in Servo mode Status is servo angle (0-360). """ _status = CFloat(transient=True) #: Minimum pulse time (in microseconds) min_pulse = CInt(544) #: Maximum pulse time (in microseconds) max_pulse = CInt(2400) def _min_pulse_changed(self): if self.traits_inited(): self.setup() def _max_pulse_changed(self): if self.traits_inited(): self.setup() def setup(self, *args, **kwargs): super().setup(*args, **kwargs) self.logger.debug("setup_servo %s %s %s %s %s %s", self, self.service, self.pin, self.min_pulse, self.max_pulse, int(round(self._status))) self._arduino.setup_servo(self.pin, self.min_pulse, self.max_pulse, int(round(self._status))) def _status_changed(self): self.logger.debug("change_servo %s %s %s", self.pin, int(round(self._status))) self._arduino.change_digital(self.pin, int(round(self._status))) def cleanup(self): self._arduino.cleanup_digital_actuator(self.pin)
class ArduinoRemotePWMActuator(AbstractArduinoActuator): """ Actuator that sends target device analog (PWM) output pin status change requests Needs `AutomateFirmata <https://github.com/tuomas2/AutomateFirmata>`_ """ _status = CFloat(transient=True) #: Target device number device = CInt def setup(self, *args, **kwargs): super().setup(*args, **kwargs) self._arduino.send_virtualwire_command( self.device, arduino_service.VIRTUALWIRE_SET_PIN_MODE, self.pin, pyfirmata.PWM) def _status_changed(self): value = min(max(self.status, 0.), 1.) value = int(round(value * 255)) # Arduino PWM has 8 bit resolution self._arduino.send_virtualwire_command( self.device, arduino_service.VIRTUALWIRE_ANALOG_MESSAGE, self.pin, value)
class Model(HasTraits): npts_x = CInt(256) npts_y = CInt(256) npts_z = CInt(109) min_x = CFloat(-2 * pi) max_x = CFloat(2 * pi) min_y = CFloat(-2 * pi) max_y = CFloat(2 * pi) min_z = CFloat(-pi) max_z = CFloat(pi) xs = Array ys = Array vals = Array minval = Float maxval = Float model_changed = Event def __init__(self, *args, **kwargs): super(Model, self).__init__(*args, **kwargs) self.compute_model() @on_trait_change("npts_+", "min_+", "max_+") def compute_model(self): def vfunc(x, y, z): return sin(x * z) * cos(y) * sin(z) + sin(0.5 * z) # Create the axes self.xs = linspace(self.min_x, self.max_x, self.npts_x) self.ys = linspace(self.min_y, self.max_y, self.npts_y) self.zs = linspace(self.min_z, self.max_z, self.npts_z) # Generate a cube of values by using newaxis to span new dimensions self.vals = vfunc(self.xs[:, newaxis, newaxis], self.ys[newaxis, :, newaxis], self.zs[newaxis, newaxis, :]) self.minval = nanmin(self.vals) self.maxval = nanmax(self.vals) self.model_changed = True
class StreamlineFactory(DataModuleFactory): """Applies the Streamline mayavi module to the given VTK data object.""" _target = Instance(modules.Streamline, ()) linetype = Trait('line', 'ribbon', 'tube', adapts='streamline_type', desc="""the type of line-like object used to display the streamline.""") seedtype = Trait('sphere', {'sphere':0, 'line':1, 'plane':2, 'point':3}, desc="""the widget used as a seed for the streamlines.""") seed_visible = Bool(True, adapts='seed.widget.enabled', desc="Control the visibility of the seed.", ) seed_scale = CFloat(1., desc="Scales the seed around its default center", ) seed_resolution = Either(None, CInt, desc='The resolution of the seed. Determines the number of ' 'seed points') integration_direction = Trait('forward', 'backward', 'both', adapts='stream_tracer.integration_direction', desc="The direction of the integration.", ) def _seedtype_changed(self): # XXX: this also acts for seed_scale and seed_resolution, but no # need to define explicit callbacks, as all the callbacks are # being called anyhow. self._target.seed.widget = widget = \ self._target.seed.widget_list[self.seedtype_] if not self.seed_scale==1.: widget.enabled = True if self.seedtype == 'line': p1 = widget.point1 p2 = widget.point2 center = (p1 + p2)/2. widget.point1 = center + self.seed_scale*(p1 - center) widget.point2 = center + self.seed_scale*(p2 - center) elif self.seedtype == 'plane': p1 = widget.point1 p2 = widget.point2 center = (p1 + p2)/2. o = widget.origin widget.point1 = center + self.seed_scale*(p1 - center) widget.point2 = center + self.seed_scale*(p2 - center) widget.origin = center + self.seed_scale*(o - center) elif self.seedtype == 'sphere': widget.radius *= self.seed_scale # XXX: Very ugly, but this is only way I have found to # propagate changes. self._target.seed.stop() self._target.seed.start() widget.enabled = self.seed_visible if self.seed_resolution is not None: widget.enabled = True if self.seedtype in ('plane', 'line'): widget.resolution = self.seed_resolution elif self.seedtype == 'sphere': widget.phi_resolution = widget.theta_resolution = \ self.seed_resolution # XXX: Very ugly, but this is only way I have found to # propagate changes. self._target.seed.stop() self._target.seed.start() widget.enabled = self.seed_visible
class VectorsFactory(DataModuleFactory): """Applies the Vectors mayavi module to the given data object source (Mayavi source, or VTK dataset). """ _target = Instance(modules.Vectors, ()) scale_factor = CFloat(1., adapts='glyph.glyph.scale_factor', desc="""the scaling applied to the glyphs. The size of the glyph is by default in drawing units.""") scale_mode = Trait('vector', {'none':'data_scaling_off', 'scalar':'scale_by_scalar', 'vector':'scale_by_vector'}, help="""the scaling mode for the glyphs ('vector', 'scalar', or 'none').""") resolution = CInt(8, desc="The resolution of the glyph created. For " "spheres, for instance, this is the number of " "divisions along theta and phi.") mask_points = Either(None, CInt, desc="If supplied, only one out of 'mask_points' " "data point is displayed. This option is useful " "to reduce the number of points displayed " "on large datasets") def _resolution_changed(self): glyph = self._target.glyph.glyph_source.glyph_source if hasattr(glyph, 'theta_resolution'): glyph.theta_resolution = self.resolution if hasattr(glyph, 'phi_resolution'): glyph.phi_resolution = self.resolution if hasattr(glyph, 'resolution'): glyph.resolution = self.resolution if hasattr(glyph, 'shaft_resolution'): glyph.shaft_resolution = self.resolution if hasattr(glyph, 'tip_resolution'): glyph.tip_resolution = self.resolution def _mask_points_changed(self): if self.mask_points is not None: self._target.glyph.mask_input_points = True self._target.glyph.mask_points.on_ratio = self.mask_points def _scale_mode_changed(self): self._target.glyph.scale_mode = self.scale_mode_ mode = Trait('2darrow', glyph_mode_dict, desc="""the mode of the glyphs.""") def _mode_changed(self): v = self._target # Workaround for different version of VTK: if hasattr(v.glyph.glyph_source, 'glyph_source'): g = v.glyph.glyph_source else: g = v.glyph if self.mode == 'point': g.glyph_source = tvtk.PointSource(radius=0, number_of_points=1) else: g.glyph_source = g.glyph_list[self.mode_] if self.mode_ == 0: g.glyph_source.glyph_type = self.mode[2:]
class Mesh(Pipeline): """ Plots a surface using grid-spaced data supplied as 2D arrays. **Function signatures**:: mesh(x, y, z, ...) x, y, z are 2D arrays, all of the same shape, giving the positions of the vertices of the surface. The connectivity between these points is implied by the connectivity on the arrays. For simple structures (such as orthogonal grids) prefer the `surf` function, as it will create more efficient data structures. For mesh defined by triangles rather than regular implicit connectivity, see the `triangular_mesh` function. """ scale_mode = Trait('none', { 'none': 'data_scaling_off', 'scalar': 'scale_by_scalar', 'vector': 'scale_by_vector' }, help="""the scaling mode for the glyphs ('vector', 'scalar', or 'none').""") scale_factor = CFloat(0.05, desc="""scale factor of the glyphs used to represent the vertices, in fancy_mesh mode. """) tube_radius = Trait(0.025, CFloat, None, help="""radius of the tubes used to represent the lines, in mesh mode. If None, simple lines are used. """) scalars = Array(help="""optional scalar data.""") mask = Array(help="""boolean mask array to suppress some data points. Note: this works based on colormapping of scalars and will not work if you specify a solid color using the `color` keyword.""") representation = Trait( 'surface', 'wireframe', 'points', 'mesh', 'fancymesh', desc="""the representation type used for the surface.""") _source_function = Callable(grid_source) _pipeline = [ ExtractEdgesFactory, GlyphFactory, TubeFactory, SurfaceFactory ] def __call_internal__(self, *args, **kwargs): """ Override the call to be able to choose whether to apply filters. """ self.source = self._source_function(*args, **kwargs) kwargs.pop('name', None) self.store_kwargs(kwargs) # Copy the pipeline so as not to modify it for the next call self.pipeline = self._pipeline[:] if not self.kwargs['representation'] in ('mesh', 'fancymesh'): self.pipeline.remove(ExtractEdgesFactory) self.pipeline.remove(TubeFactory) self.pipeline.remove(GlyphFactory) self.pipeline = [ PolyDataNormalsFactory, ] + self.pipeline else: if self.kwargs['tube_radius'] is None: self.pipeline.remove(TubeFactory) if not self.kwargs['representation'] == 'fancymesh': self.pipeline.remove(GlyphFactory) self.kwargs['representation'] = 'surface' return self.build_pipeline()
class BarChart(Pipeline): """ Plots vertical glyphs (like bars) scaled vertical, to do histogram-like plots. This functions accepts a wide variety of inputs, with positions given in 2-D or in 3-D. **Function signatures**:: barchart(s, ...) barchart(x, y, s, ...) barchart(x, y, f, ...) barchart(x, y, z, s, ...) barchart(x, y, z, f, ...) If only one positional argument is passed, it can be a 1-D, 2-D, or 3-D array giving the length of the vectors. The positions of the data points are deducted from the indices of array, and an uniformly-spaced data set is created. If 3 positional arguments (x, y, s) are passed the last one must be an array s, or a callable, f, that returns an array. x and y give the 2D coordinates of positions corresponding to the s values. If 4 positional arguments (x, y, z, s) are passed, the 3 first are arrays giving the 3D coordinates of the data points, and the last one is an array s, or a callable, f, that returns an array giving the data value. """ _source_function = Callable(vertical_vectors_source) _pipeline = [ VectorsFactory, ] mode = Trait('cube', bar_mode_dict, desc='The glyph used to represent the bars.') lateral_scale = CFloat(0.9, desc='The lateral scale of the glyph, ' 'in units of the distance between nearest points') auto_scale = true(desc='whether to compute automatically the ' 'lateral scaling of the glyphs. This might be ' 'computationally expensive.') def __call_internal__(self, *args, **kwargs): """ Override the call to be able to scale automatically the axis. """ g = Pipeline.__call_internal__(self, *args, **kwargs) gs = g.glyph.glyph_source # Use a cube source for glyphs. if not 'mode' in kwargs: gs.glyph_source = gs.glyph_dict['cube_source'] # Position the glyph tail on the point. gs.glyph_position = 'tail' gs.glyph_source.center = (0.0, 0.0, 0.5) g.glyph.glyph.orient = False if not 'color' in kwargs: g.glyph.color_mode = 'color_by_scalar' if not 'scale_mode' in kwargs: g.glyph.scale_mode = 'scale_by_vector_components' g.glyph.glyph.clamping = False # The auto-scaling code. It involves finding the minimum # distance between points, which can be very expensive. We # shortcut this calculation for structured data if len(args) == 1 or self.auto_scale: min_axis_distance = 1 else: x, y, z = g.mlab_source.x, g.mlab_source.y, g.mlab_source.z min_axis_distance = \ tools._min_axis_distance(x, y, z) scale_factor = g.glyph.glyph.scale_factor * min_axis_distance lateral_scale = kwargs.pop('lateral_scale', self.lateral_scale) try: g.glyph.glyph_source.glyph_source.y_length = \ lateral_scale / (scale_factor) g.glyph.glyph_source.glyph_source.x_length = \ lateral_scale / (scale_factor) except TraitError: " Not all types of glyphs have controlable y_length and x_length" return g
class DataRange2D(BaseDataRange): """ A range on (2-D) image data. In a mathematically general sense, a 2-D range is an arbitrary region in the plane. Arbitrary regions are difficult to implement well, so this class supports only rectangular regions for now. """ # The actual value of the lower bound of this range. To set it, use # **low_setting**. low = Property # (2,) array of lower-left x,y # The actual value of the upper bound of this range. To set it, use # **high_setting**. high = Property # (2,) array of upper-right x,y # Property for the lower bound of this range (overrides AbstractDataRange). low_setting = Property # Property for the upper bound of this range (overrides AbstractDataRange). high_setting = Property # The 2-D grid range is actually implemented as two 1-D ranges, which can # be accessed individually. They can also be set to new DataRange1D # instances; in that case, the DataRange2D's sources are removed from # its old 1-D dataranges and added to the new one. # Property for the range in the x-dimension. x_range = Property # Property for the range in the y-dimension. y_range = Property # Do "auto" bounds imply an exact fit to the data? (One Boolean per # dimension) If False, the bounds pad a little bit of margin on either # side. tight_bounds = Tuple(Bool(True), Bool(True)) # The minimum percentage difference between low and high for each # dimension. That is, (high-low) >= epsilon * low. epsilon = Tuple(CFloat(1.0e-4), CFloat(1.0e-4)) #------------------------------------------------------------------------ # Private traits #------------------------------------------------------------------------ # DataRange1D for the x-dimension. _xrange = Instance(DataRange1D, args=()) # DataRange1D for the y-dimension. _yrange = Instance(DataRange1D, args=()) #------------------------------------------------------------------------ # AbstractRange interface #------------------------------------------------------------------------ def clip_data(self, data): """ Returns a list of data values that are within the range. Implements AbstractDataRange. """ return compress(self.mask_data(data), data, axis=0) def mask_data(self, data): """ Returns a mask array, indicating whether values in the given array are inside the range. Implements AbstractDataRange. """ x_points, y_points = transpose(data) x_mask = (x_points >= self.low[0]) & (x_points <= self.high[0]) y_mask = (y_points >= self.low[1]) & (y_points <= self.high[1]) return x_mask & y_mask def bound_data(self, data): """ Not implemented for this class. """ raise NotImplementedError("bound_data() has not been implemented " "for 2d pointsets.") def set_bounds(self, low, high): """ Sets all the bounds of the range simultaneously. Implements AbstractDataRange. Parameters ---------- low : (x,y) Lower-left corner of the range. high : (x,y) Upper right corner of the range. """ self._do_set_low_setting(low, fire_event=False) self._do_set_high_setting(high) #------------------------------------------------------------------------ # Public methods #------------------------------------------------------------------------ def __init__(self, *args, **kwargs): super(DataRange2D, self).__init__(*args, **kwargs) def reset(self): """ Resets the bounds of this range. """ self.high_setting = ('auto', 'auto') self.low_setting = ('auto', 'auto') self.refresh() def refresh(self): """ If any of the bounds is 'auto', this method refreshes the actual low and high values from the set of the view filters' data sources. """ if 'auto' not in self.low_setting and \ 'auto' not in self.high_setting: # If the user has hard-coded bounds, then refresh() doesn't do # anything. return else: self._refresh_bounds() #------------------------------------------------------------------------ # Private methods #------------------------------------------------------------------------ def _refresh_bounds(self): self._xrange.refresh() self._yrange.refresh() #------------------------------------------------------------------------ # Property getters and setters #------------------------------------------------------------------------ def _get_low(self): return (self._xrange.low, self._yrange.low) def _set_low(self, val): return self._set_low_setting(val) def _get_low_setting(self): return (self._xrange.low_setting, self._yrange.low_setting) def _set_low_setting(self, val): self._do_set_low_setting(val) def _do_set_low_setting(self, val, fire_event=True): self._xrange.low_setting = val[0] self._yrange.low_setting = val[1] def _get_high(self): return (self._xrange.high, self._yrange.high) def _set_high(self, val): return self._set_high_setting(val) def _get_high_setting(self): return (self._xrange.high_setting, self._yrange.high_setting) def _set_high_setting(self, val): self._do_set_high_setting(val) def _do_set_high_setting(self, val, fire_event=True): self._xrange.high_setting = val[0] self._yrange.high_setting = val[1] def _get_x_range(self): return self._xrange def _set_x_range(self, newrange): self._set_1d_range("_xdata", self._xrange, newrange) self._xrange = newrange def _get_y_range(self): return self._yrange def _set_y_range(self, newrange): self._set_1d_range("_ydata", self._yrange, newrange) self._yrange = newrange def _set_1d_range(self, dataname, oldrange, newrange): # dataname is the name of the underlying 1d data source of the # ImageData instances in self.sources, e.g. "_xdata" or "_ydata" for source in self.sources: source1d = getattr(source, dataname, None) if source1d: if oldrange: oldrange.remove(source1d) if newrange: newrange.add(source1d) return #------------------------------------------------------------------------ # Event handlers #------------------------------------------------------------------------ def _sources_items_changed(self, event): for source in event.removed: source.on_trait_change(self.refresh, "data_changed", remove=True) for source in event.added: source.on_trait_change(self.refresh, "data_changed") # the _xdata and _ydata of the sources may be created anew on every # access, so we can't just add/delete from _xrange and _yrange sources # based on object identity. So recreate lists each time: self._xrange.sources = [s._xdata for s in self.sources] self._yrange.sources = [s._ydata for s in self.sources] self.refresh() def _sources_changed(self, old, new): for source in old: source.on_trait_change(self.refresh, "data_changed", remove=True) for source in new: source.on_trait_change(self.refresh, "data_changed") # the _xdata and _ydata of the sources may be created anew on every # access, so we can't just add/delete from _xrange and _yrange sources # based on object identity. So recreate lists each time: self._xrange.sources = [s._xdata for s in self.sources] self._yrange.sources = [s._ydata for s in self.sources] self.refresh() @on_trait_change("_xrange.updated,_yrange.updated") def _subranges_updated(self): self.updated = True
class Model(HasTraits): #Traits view definitions: traits_view = View( Group(Item('function'), HGroup(Item('npts_x', label="Number X Points"), Item('npts_y', label="Number Y Points")), HGroup(Item('min_x', label="Min X value"), Item('max_x', label="Max X value")), HGroup(Item('min_y', label="Min Y value"), Item('max_y', label="Max Y value"))), buttons=["OK", "Cancel"]) function = Str("tanh(x**2+y)*cos(y)*jn(0,x+y*2)") npts_x = CInt(400) npts_y = CInt(200) min_x = CFloat(-2*pi) max_x = CFloat(2*pi) min_y = CFloat(-1.5*pi) max_y = CFloat(1.5*pi) xs = Array ys = Array zs = Array minz = Float maxz = Float model_changed = Event def __init__(self, *args, **kwargs): super(Model, self).__init__(*args, **kwargs) self.compute_model() def compute_model(self): # The xs and ys used for the image plot range need to be the # edges of the cells. self.xs = linspace(self.min_x, self.max_x, self.npts_x+1) self.ys = linspace(self.min_y, self.max_y, self.npts_y+1) # The grid of points at which we will evaluate the 2D function # is located at cell centers, so use halfsteps from the # min/max values (which are edges) xstep = (self.max_x - self.min_x) / self.npts_x #ystep = (self.max_y - self.min_y) / self.npts_y gridx = linspace(self.min_x+xstep/2, self.max_x-xstep/2, self.npts_x) gridy = linspace(self.min_y+xstep/2, self.max_y-xstep/2, self.npts_y) x, y = meshgrid(gridx, gridy) try: d = dict(x=x, y=y) exec "from scipy import *" in d exec "from scipy.special import *" in d self.zs = eval(self.function, d) self.minz = nanmin(self.zs) self.maxz = nanmax(self.zs) self.model_changed = True self._function = self.function except: self.set(function = self._function, trait_change_notify=False) def _anytrait_changed(self, name, value): if name in ['function', 'npts_x', 'npts_y', 'min_x', 'max_x', 'min_y', 'max_y']: self.compute_model()
class RangeOp(HasStrictTraits): """Apply a range gate to a cytometry experiment. Attributes ---------- name : Str The operation name. Used to name the new metadata field in the experiment that's created by apply() channel : Str The name of the channel to apply the range gate. low : Float The lowest value to include in this gate. high : Float The highest value to include in this gate. Examples -------- >>> range = flow.RangeOp() >>> range.name = "Y2-A+" >>> range.channel = 'Y2-A' >>> range.low = 0.3 >>> range.high = 0.8 >>> >>> ex3 = range.apply(ex2) Alternately (in an IPython notebook with `%matplotlib notebook`) >>> r = RangeOp(name = 'Y2-A+', ... channel = 'Y2-A') >>> rv = r.default_view() >>> rv.interactive = True >>> rv.plot(ex2) >>> ### draw a range on the plot ### >>> ex3 = r.apply(ex2) """ # traits id = Constant('edu.mit.synbio.cytoflow.operations.range') friendly_id = Constant('Range') name = CStr() channel = Str() low = CFloat() high = CFloat() def apply(self, experiment): """Applies the threshold to an experiment. Parameters ---------- experiment : Experiment the old_experiment to which this op is applied Returns ------- a new experiment, the same as old_experiment but with a new column the same as the operation name. The bool is True if the event's measurement in self.channel is greater than self.low and less than self.high; it is False otherwise. """ if not experiment: raise util.CytoflowOpError("No experiment specified") # make sure name got set! if not self.name: raise util.CytoflowOpError("You have to set the gate's name " "before applying it!") if self.name in experiment.data.columns: raise util.CytoflowOpError( "Experiment already has a column named {0}".format(self.name)) if not self.channel: raise util.CytoflowOpError("Channel not specified") if not self.channel in experiment.channels: raise util.CytoflowOpError( "Channel {0} not in the experiment".format(self.channel)) if self.high <= self.low: raise util.CytoflowOpError("range high must be > range low") if self.high <= experiment[self.channel].min(): raise util.CytoflowOpError("range high must be > {0}".format( experiment[self.channel].min())) if self.low >= experiment[self.channel].max: raise util.CytoflowOpError("range low must be < {0}".format( experiment[self.channel].max())) gate = experiment[self.channel].between(self.low, self.high) new_experiment = experiment.clone() new_experiment.add_condition(self.name, "bool", gate) new_experiment.history.append(self.clone_traits()) return new_experiment def default_view(self, **kwargs): return RangeSelection(op=self, **kwargs)
class RobotView(HasTraits): size = (300, 300) view = View(Group(Item('plot', editor=ComponentEditor(size=size), show_label=False), Item(name='velocity_x'), Item(name='velocity_y'), orientation="vertical"), resizable=True, width=size[0], height=size[1]) wheel_line = Instance(Line, args=()) velocity_x = CFloat(0.0, desc="Velocity in the x direction", label="Vx") velocity_y = CFloat(0.0, desc="Velocity in the y direction", label="Vy") def __init__(self, velocity_callback=None, x_range=DataRange1D(low=0, high=size[0]), y_range=DataRange1D(low=0, high=size[1]), *args, **kwargs): super().__init__(*args, **kwargs) self.robot_bounding_box = { "x": [100, 200, 200, 100], "y": [80, 80, 220, 220] } self.robot_wheels = [ { # Bottom-left "x": [90, 110, 110, 90], "y": [90, 90, 140, 140] }, { # Bottom-right "x": [190, 210, 210, 190], "y": [90, 90, 140, 140] }, { # Top-right "x": [90, 110, 110, 90], "y": [160, 160, 210, 210] }, { # Top-left "x": [190, 210, 210, 190], "y": [160, 160, 210, 210] }, ] self.plot_data = ArrayPlotData(robot_x=self.robot_bounding_box['x'], robot_y=self.robot_bounding_box['y'], wheel_bl_x=self.robot_wheels[0]['x'], wheel_bl_y=self.robot_wheels[0]['y'], wheel_br_x=self.robot_wheels[1]['x'], wheel_br_y=self.robot_wheels[1]['y'], wheel_tr_x=self.robot_wheels[2]['x'], wheel_tr_y=self.robot_wheels[2]['y'], wheel_tl_x=self.robot_wheels[3]['x'], wheel_tl_y=self.robot_wheels[3]['y'], arrow_x=[0, 100, 200], arrow_y=[0, 100, 100], arrowhead_x=[150, 200], arrowhead_y=[150, 200]) self.x_range = x_range self.y_range = y_range self.velocity_callback = velocity_callback velocity_tool = VelocityTool((150, 150), (300, 300), self.update_velocity, self.hover_velocity) # Show starting plot self.plot = self.plot_robot() self.plot.tools.append(velocity_tool) self.plot_velocity() def update_velocity(self, velocity): target_x = 150 + velocity[0] * 150 target_y = 150 + velocity[1] * 150 self.velocity_x = (target_x - 150) / 150 self.velocity_y = (target_y - 150) / 150 self.plot_arrow((150, 150), (target_x, target_y), (1.0, 0, 0, 0.9), 'vel') if self.velocity_callback is not None: self.velocity_callback(self.velocity_x, self.velocity_y) def hover_velocity(self, hover_velocity): target_x = 150 + hover_velocity[0] * 150 target_y = 150 + hover_velocity[1] * 150 self.plot_arrow((150, 150), (target_x, target_y), (0.0, 0.0, 0.0, 0.25), 'hov_vel') pass def plot_robot(self): plot = Plot(self.plot_data) plot.range2d.x_range = self.x_range plot.range2d.y_range = self.y_range plot.x_mapper = LinearMapper(range=self.x_range) plot.y_mapper = LinearMapper(range=self.y_range) plot.title = "robot" plot.plot(('robot_x', 'robot_y'), type='polygon', marker='dot', marker_size=1.0, color=(0, 0, 0, 0.8)) wheel_outline_color = (0, 0, 0, 1.0) wheel_color = (0, 0, 0, 0.8) plot.plot(('wheel_bl_x', 'wheel_bl_y'), type='polygon', marker='dot', marker_size=1.0, face_color=wheel_color, color=wheel_outline_color) plot.plot(('wheel_br_x', 'wheel_br_y'), type='polygon', marker='dot', marker_size=1.0, face_color=wheel_color, color=wheel_outline_color) plot.plot(('wheel_tr_x', 'wheel_tr_y'), type='polygon', marker='dot', marker_size=1.0, face_color=wheel_color, color=wheel_outline_color) plot.plot(('wheel_tl_x', 'wheel_tl_y'), type='polygon', marker='dot', marker_size=1.0, face_color=wheel_color, color=wheel_outline_color) return plot def plot_velocity(self): origin_x = self.size[0] / 2 origin_y = self.size[1] / 2 self.plot_arrow((origin_x, origin_y), (origin_x, origin_y)) pass def plot_arrow(self, from_point, to_point, color=(1.0, 1.0, 0.0, 0.1), label="arrow"): """ Plot an arrow between two points :param from_point: The starting point of the arrow :param to_point: The ending point of the arrow :param color The color of the arrow :return: """ if to_point == from_point: return # compute the length of the arrow and angle with the horizontal r = np.sqrt( pow((to_point[0] - from_point[0]), 2) + pow((to_point[1] - from_point[1]), 2)) theta = np.arccos((to_point[0] - from_point[0]) / r) # length adjustment for arrow side points r_factor = 0.70 + 0.15 * (r / 212.5) # angle offset for arrow side points angle = 0.25 - 0.125 * (r / 212.5) # Compute where the side points should be drawn pos_neg = 1 if (from_point[1] < to_point[1]) else -1 x1 = from_point[0] + r_factor * r * np.cos(theta - angle) x2 = from_point[0] + r_factor * r * np.cos(theta + angle) y1 = from_point[1] + pos_neg * r_factor * r * np.sin(theta - angle) y2 = from_point[1] + pos_neg * r_factor * r * np.sin(theta + angle) update = True if f'{label}_x' in self.plot.data.arrays.keys( ) else False # Update middle line segment self.plot.data.set_data(f'{label}_x', [from_point[0], to_point[0]]) self.plot.data.set_data(f'{label}_y', [from_point[1], to_point[1]]) # Draw arrowhead self.plot.data.set_data(f'{label}_arrowhead_x', [to_point[0], x1, x2, to_point[0]]) self.plot.data.set_data(f'{label}_arrowhead_y', [to_point[1], y1, y2, to_point[1]]) if not update: self.plot.plot( (f'{label}_x', f'{label}_y'), type='line', marker='dot', line_width=2.0, face_color=color, color=color, ) self.plot.plot( (f'{label}_arrowhead_x', f'{label}_arrowhead_y'), type='polygon', marker='dot', line_width=1.0, edge_color=color, face_color=color, )