class MessagesResponse(Response): """ Expected response from api/messages """ Limit = Int() Total = Int() Messages = List(Message)
class QtTextCompleter(RawWidget): """Simple text editor supporting completion. """ #: Text being edited by this widget. text = d_(Unicode()) #: Static list of entries used to propose completion. This member value is #: not updated by the entries_updater. entries = d_(List()) #: Callable to use to refresh the completions. entries_updater = d_(Callable()) #: Delimiters marking the begining and end of completed section. delimiters = d_(Tuple(Unicode(), ('{', '}'))) hug_width = 'ignore' features = Feature.FocusEvents #: Flag avoiding circular updates. _no_update = Bool(False) #: Reference to the QCompleter used by the widget. _completer = Value() def create_widget(self, parent): """Finishes initializing by creating the underlying toolkit widget. """ widget = QCompletableTexEdit(parent) self._completer = QDelimitedCompleter(widget, self.delimiters, self.entries, self.entries_updater) widget.completer = self._completer widget.setText(self.text) widget.textChanged.connect(self.update_object) return widget def update_object(self): """ Handles the user entering input data in the edit control. """ if (not self._no_update) and self.activated: value = self.get_widget().toPlainText() self._no_update = True self.text = value self._no_update = False def focus_lost(self): """Notify the completer the focus was lost. """ self._completer.on_focus_lost() def _post_setattr_text(self, old, new): """Updates the editor when the object changes externally to the editor. """ if (not self._no_update) and self.get_widget(): self._no_update = True self.get_widget().setText(new) self._no_update = False def _post_setattr_entries(self, old, new): """Updates the completer entries. """ if self.proxy_is_active and self._completer: self._completer._update_entries(new)
class Device(Model): """ The standard device. This is a standard model used throughout the application. An instance of this is configured by specifying a 'DeviceDriver' in a plugin manifest. It simply delegates connection and handling to the selected transport and protocol respectively. """ #: Internal model for drawing the preview on screen area = Instance(AreaBase) #: The declaration that defined this device declaration = Typed(extensions.DeviceDriver).tag(config=True) #: Protocols supported by this device (ex the HPGLProtocol) protocols = List(extensions.DeviceProtocol) #: Transports supported by this device (ex the SerialPort transports = List(extensions.DeviceTransport) #: The active transport connection = Instance(DeviceTransport).tag(config=True) #: List of jobs that were run on this device jobs = List(Model).tag(config=True) #: List of jobs queued to run on this device queue = List(Model).tag(config=True) #: Current job being processed job = Instance(Model) #.tag(config=True) #: The device specific config config = Instance(DeviceConfig, ()).tag(config=True) #: Position. Defaults to x,y,z. The protocol can #: handle this however necessary. position = ContainerList(default=[0, 0, 0]) #: Origin position. Defaults to [0, 0, 0]. The system will translate #: jobs to the origin so multiple can be run. origin = ContainerList(default=[0, 0, 0]) #: Device is currently busy processing a job busy = Bool() #: Status status = Unicode() def _default_connection(self): if not self.transports: return None declaration = self.transports[0] transport = declaration.factory() transport.declaration = declaration transport.protocol = self._default_protocol() return transport def _default_protocol(self): if not self.protocols: return None declaration = self.protocols[0] protocol = declaration.factory() protocol.declaration = declaration return protocol def _default_area(self): """ Create the area based on the size specified by the Device Driver """ d = self.declaration area = AreaBase() w = parse_unit(d.width) if d.length: h = parse_unit(d.length) else: h = 900000 area.size = [w, h] return area @observe('declaration.width', 'declaration.length') def _refresh_area(self, change): self.area = self._default_area() @contextmanager def device_busy(self): """ Mark the device as busy """ self.busy = True try: yield finally: self.busy = False @contextmanager def device_connection(self, test=False): """ """ connection = self.connection try: #: Create a test connection if necessary if test: self.connection = TestTransport( protocol=connection.protocol, declaration=connection.declaration) #: Connect yield self.connection finally: #: Restore if necessary if test: self.connection = connection @defer.inlineCallbacks def test(self): """ Execute a test job on the device. This creates and submits new job that is simply a small square. """ raise NotImplementedError def transform(self, path): """ Apply the device output transform to the given path. This is used by other plugins that may need to display or work with tranformed output. Parameters ---------- path: QPainterPath Path to transform Returns ------- path: QPainterPath """ config = self.config t = QtGui.QTransform() #: Order matters! if config.scale: #: Do final output scaling t.scale(*config.scale) if config.rotation: #: Do final output rotation t.rotate(config.rotation) #: TODO: Translate back to 0,0 so all coordinates are positive path = path * t return path def init(self, job): """ Initialize the job. This should do any final path manipulation required by the device (or as specified by the config) and any filters should be applied here (overcut, blade offset compensation, etc..). The connection is not active at this stage. Parameters ----------- job: inkcut.job.models.Job instance The job to handle. Returns -------- model: QtGui.QPainterPath instance or Deferred that resolves to a QPainterPath if heavy processing is needed. This path is then interpolated and sent to the device. """ log.debug("device | init {}".format(job)) config = self.config #: Set the speed of this device for tracking purposes units = config.speed_units.split("/")[0] job.info.speed = from_unit(config.speed, units) #: Get the internal QPainterPath "model" model = job.model #: Transform the path to the device coordinates model = self.transform(model) if job.feed_to_end: #: Move the job to the new origin x, y, z = self.origin model.translate(x, -y) #: TODO: Apply filters here #: Return the transformed model return model @defer.inlineCallbacks def connect(self): """ Connect to the device. By default this delegates handling to the active transport or connection handler. Returns ------- result: Deferred or None May return a Deferred object that the process will wait for completion before continuing. """ log.debug("device | connect") yield defer.maybeDeferred(self.connection.connect) cmd = self.config.commands_connect if cmd: yield defer.maybeDeferred(self.connection.write, cmd) def move(self, position, absolute=True): """ Move to the given position. By default this delegates handling to the active protocol. Parameters ---------- position: List of coordinates to move to. Desired position to move or move to (if using absolute coordinates). absolute: bool Position is in absolute coordinates Returns ------- result: Deferred or None May return a deferred object that the process will wait for completion before continuing. """ if absolute: #: Clip everything to never go below zero in absolute mode position = [max(0, p) for p in position] self.position = position else: #: Convert to relative to absolute for the UI p = self.position p[0] += position[0] p[1] += position[1] self.position = p result = self.connection.protocol.move(*position, absolute=absolute) if result: return result def finish(self): """ Finish the job applying any cleanup necessary. """ log.debug("device | finish") return self.connection.protocol.finish() @defer.inlineCallbacks def disconnect(self): """ Disconnect from the device. By default this delegates handling to the active transport or connection handler. """ log.debug("device | disconnect") cmd = self.config.commands_disconnect if cmd: yield defer.maybeDeferred(self.connection.write, cmd) yield defer.maybeDeferred(self.connection.disconnect) @defer.inlineCallbacks def submit(self, job, test=False): """ Submit the job to the device. If the device is currently running a job it will be queued and run when this is finished. This handles iteration over the path model defined by the job and sending commands to the actual device using roughly the procedure is as follows: device.connect() model = device.init(job) for cmd in device.process(model): device.handle(cmd) device.finish() device.disconnect() Subclasses provided by your own DeviceDriver may reimplement this to handle path interpolation however needed. The return value is ignored. The live plot view will update whenever the device.position object is updated. On devices with lower cpu/gpu capabilities this should be updated sparingly (ie the raspberry pi). Parameters ----------- job: Instance of `inkcut.job.models.Job` The job to execute on the device test: bool Do a test run. This specifies whether the commands should be sent to the actual device or not. If True, the connection will be replaced with a virtual connection that captures all the command output. """ log.debug("device | submit {}".format(job)) try: #: Only allow one job at a time if self.busy: queue = self.queue[:] queue.append(job) self.queue = queue #: Copy and reassign so the UI updates log.info("Job {} put in device queue".format(job)) return with self.device_busy(): #: Set the current the job self.job = job self.status = "Initializing job" #: Get the time to sleep based for each unit of movement config = self.config #: Rate px/ms if config.custom_rate >= 0: rate = config.custom_rate elif self.connection.always_spools or config.spooled: rate = 0 elif config.interpolate: if config.step_time > 0: rate = config.step_size / float(config.step_time) else: rate = 0 # Undefined else: rate = from_unit( config.speed, # in/s or cm/s config.speed_units.split("/")[0]) / 1000.0 # Device model is updated in real time model = yield defer.maybeDeferred(self.init, job) #: Local references are faster info = job.info #: Determine the length for tracking progress whole_path = QtGui.QPainterPath() #: Some versions of Qt seem to require a value in #: toSubpathPolygons m = QtGui.QTransform.fromScale(1, 1) for path in model.toSubpathPolygons(m): for i, p in enumerate(path): whole_path.lineTo(p) total_length = whole_path.length() total_moved = 0 log.debug("device | Path length: {}".format(total_length)) #: So a estimate of the duration can be determined info.length = total_length info.speed = rate * 1000 #: Convert to px/s #: Waiting for approval info.status = 'waiting' #: If marked for auto approve start now if info.auto_approve: info.status = 'approved' else: #: Check for approval before starting yield defer.maybeDeferred(info.request_approval) if info.status != 'approved': self.status = "Job cancelled" return #: Update stats info.status = 'running' info.started = datetime.now() self.status = "Connecting to device" with self.device_connection(test or config.test_mode) as connection: self.status = "Processing job" try: yield defer.maybeDeferred(self.connect) #: Write startup command if config.commands_before: yield defer.maybeDeferred(connection.write, config.commands_before) self.status = "Working..." #: For point in the path for (d, cmd, args, kwargs) in self.process(model): #: Check if we paused if info.paused: self.status = "Job paused" #: Sleep until resumed, cancelled, or the #: connection drops while (info.paused and not info.cancelled and connection.connected): yield async_sleep(300) # ms #: Check for cancel for non interpolated jobs if info.cancelled: self.status = "Job cancelled" info.status = 'cancelled' break elif not connection.connected: self.status = "connection error" info.status = 'error' break #: Invoke the command #: If you want to let the device handle more complex #: commands such as curves do it in process and handle yield defer.maybeDeferred(cmd, *args, **kwargs) total_moved += d #: d should be the device must move in px #: so wait a proportional amount of time for the device #: to catch up. This avoids buffer errors from dumping #: everything at once. #: Since sending is way faster than cutting #: we must delay (without blocking the UI) before #: sending the next command or the device's buffer #: quickly gets filled and crappy china piece cutters #: get all jacked up. If the transport sends to a spooled #: output (such as a printer) this can be set to 0 log.debug("Rate is :{}".format(rate)) if rate > 0: # log.debug("d={}, delay={} t={}".format( # d, delay, d/delay # )) yield async_sleep(d / rate) #: TODO: Check if we need to update the ui #: Set the job progress based on how far we've gone if total_length > 0: info.progress = int( max( 0, min(100, 100 * total_moved / total_length))) if info.status != 'error': #: We're done, send any finalization commands yield defer.maybeDeferred(self.finish) #: Write finalize command if config.commands_after: yield defer.maybeDeferred(connection.write, config.commands_after) #: Update stats info.ended = datetime.now() #: If not cancelled or errored if info.status == 'running': info.done = True info.status = 'complete' except Exception as e: log.error(e) raise finally: if connection.connected: yield defer.maybeDeferred(self.disconnect) #: Set the origin if job.feed_to_end and job.info.status == 'complete': self.origin = self.position #: If the user didn't cancel, set the origin and #: Process any jobs that entered the queue while this was running if self.queue and not job.info.cancelled: queue = self.queue[:] job = queue.pop(0) #: Pull the first job off the queue log.info("Rescheduling {} from queue".format(job)) self.queue = queue #: Copy and reassign so the UI updates #: Call a minute later timed_call(60000, self.submit, job) except Exception as e: log.error(e) raise def process(self, model): """ Process the path model of a job and return each command within the job. Parameters ---------- model: QPainterPath The path to process Returns ------- generator: A list or generator object that yields each command to invoke on the device and the distance moved. In the format (distance, cmd, args, kwargs) """ config = self.config #: Previous point _p = QtCore.QPointF(self.origin[0], self.origin[1]) #: Do a final translation since Qt's y axis is reversed t = QtGui.QTransform.fromScale(1, -1) model = model * t #: Determine if interpolation should be used skip_interpolation = self.connection.always_spools or config.spooled or not config.interpolate # speed = distance/seconds # So distance/speed = seconds to wait step_size = config.step_size if not skip_interpolation and step_size <= 0: raise ValueError("Cannot have a step size <= 0!") try: #: Some versions of Qt seem to require a value in toSubpathPolygons m = QtGui.QTransform.fromScale(1, 1) for path in model.toSubpathPolygons(m): #: And then each point within the path #: this is a polygon for i, p in enumerate(path): #: Head state # 0 move, 1 cut z = 0 if i == 0 else 1 #: Make a subpath subpath = QtGui.QPainterPath() subpath.moveTo(_p) subpath.lineTo(p) #: Update the last point _p = p #: Total length l = subpath.length() #: If the device does not support streaming #: the path interpolation is skipped entirely if skip_interpolation: x, y = p.x(), p.y() yield (l, self.move, ([x, y, z], ), {}) continue #: Where we are within the subpath d = 0 #: Interpolate path in steps of dl and ensure we get #: _p and p (t=0 and t=1) #: This allows us to cancel mid point while d <= l: #: Now set d to the next point by step_size #: if the end of the path is less than the step size #: use the minimum of the two dl = min(l - d, step_size) #: Now find the point at the given step size #: the first point d=0 so t=0, the last point d=l so t=1 t = subpath.percentAtLength(d) sp = subpath.pointAtPercent(t) #if d == l: # break #: Um don't we want to send the last point?? #: -y because Qt's axis is from top to bottom not bottom #: to top x, y = sp.x(), sp.y() yield (dl, self.move, ([x, y, z], ), {}) #: When we reached the end but instead of breaking above #: with a d < l we do it here to ensure we get the last #: point if d == l: #: We reached the end break #: Add step size d += dl #: Make sure we get the endpoint ep = model.currentPosition() yield (0, self.move, ([ep.x(), ep.y(), 0], ), {}) except Exception as e: log.error("device | processing error: {}".format(e)) raise e def _observe_status(self, change): """ Whenever the status changes, log it """ log.info("device | {}".format(self.status)) def _observe_job(self, change): """ Save the previous jobs """ if change['type'] == 'update': job = change['value'] if job not in self.jobs: jobs = self.jobs[:] jobs.append(job) self.jobs = jobs
class InstrumentManagerPlugin(HasPreferencesPlugin): """The instrument plugin manages the instrument drivers and their use. """ #: List of the known instrument profile ids. profiles = List() #: List of instruments for which at least one driver is declared. instruments = List() #: List of registered intrument users. #: Only registered users can be granted the use of an instrument. users = List() #: List of registered instrument starters. starters = List() #: List of registered connection types. connections = List() #: List of registered settings. settings = List() #: Currently used profiles. #: This dict should be edited by user code. used_profiles = Dict() def start(self): """Start the plugin lifecycle by collecting all contributions. """ super(InstrumentManagerPlugin, self).start() core = self.workbench.get_plugin('enaml.workbench.core') core.invoke_command('ecpy.app.errors.enter_error_gathering') state = core.invoke_command('ecpy.app.states.get', {'state_id': 'ecpy.app.directory'}) i_dir = os.path.join(state.app_directory, 'instruments') # Create instruments subfolder if it does not exist. if not os.path.isdir(i_dir): os.mkdir(i_dir) p_dir = os.path.join(i_dir, 'profiles') # Create profiles subfolder if it does not exist. if not os.path.isdir(p_dir): os.mkdir(p_dir) self._profiles_folders = [p_dir] self._users = ExtensionsCollector(workbench=self.workbench, point=USERS_POINT, ext_class=InstrUser, validate_ext=validate_user) self._users.start() self._starters = ExtensionsCollector(workbench=self.workbench, point=STARTERS_POINT, ext_class=Starter, validate_ext=validate_starter) self._starters.start() checker = make_extension_validator(Connection, ('new', ), ('id', 'description')) self._connections = ExtensionsCollector(workbench=self.workbench, point=CONNECTIONS_POINT, ext_class=Connection, validate_ext=checker) self._connections.start() checker = make_extension_validator(Settings, ('new', ), ('id', 'description')) self._settings = ExtensionsCollector(workbench=self.workbench, point=SETTINGS_POINT, ext_class=Settings, validate_ext=checker) self._settings.start() checker = make_extension_validator(ManufacturerAlias, (), ( 'id', 'aliases', )) self._aliases = ExtensionsCollector(workbench=self.workbench, point=ALIASES_POINT, ext_class=ManufacturerAlias, validate_ext=checker) self._aliases.start() self._drivers = DeclaratorsCollector(workbench=self.workbench, point=DRIVERS_POINT, ext_class=[Driver, Drivers]) self._drivers.start() for contrib in ('users', 'starters', 'connections', 'settings'): self._update_contribs(contrib, None) err = False details = {} for d_id, d_infos in self._drivers.contributions.items(): res, tb = d_infos.validate(self) if not res: err = True details[d_id] = tb if err: core.invoke_command('ecpy.app.errors.signal', { 'kind': 'ecpy.driver-validation', 'details': details }) # TODO providing in app a way to have a splash screen while starting to # let the user know what is going on would be nice # TODO handle dynamic addition of drivers by observing contributions # and updating the manufacturers infos accordingly. # should also observe manufacturer aliases self._refresh_profiles() self._bind_observers() core.invoke_command('ecpy.app.errors.exit_error_gathering') def stop(self): """Stop the plugin and remove all observers. """ self._unbind_observers() for contrib in ('drivers', 'users', 'starters', 'connections', 'settings'): getattr(self, '_' + contrib).stop() def create_connection(self, connection_id, infos, read_only=False): """Create a connection and initialize it. Parameters ---------- connection_id : unicode Id of the the connection to instantiate. infos : dict Dictionarry to use to initialize the state of the connection. read_only : bool Should the connection be created as read-only. Returns ------- connection : BaseConnection Ready to use widget. """ c_decl = self._connections.contributions[connection_id] conn = c_decl.new(self.workbench, infos, read_only) if conn.declaration is None: conn.declaration = c_decl return conn def create_settings(self, settings_id, infos, read_only=False): """Create a settings and initialize it. Parameters ---------- settings_id : unicode Id of the the settings to instantiate. infos : dict Dictionary to use to initialize the state of the settings. read_only : bool Should the settings be created as read-only. Returns ------- connection : BaseSettings Ready to use widget. """ if settings_id is None: msg = 'No id was found for the settings whose infos are %s' logger.warn(msg, infos) return None s_decl = self._settings.contributions[settings_id] sett = s_decl.new(self.workbench, infos, read_only) if sett.declaration is None: sett.declaration = s_decl return sett def get_drivers(self, drivers): """Query drivers class and the associated starters. Parameters ---------- drivers : list List of driver ids for which the matching class should be returned. Returns ------- drivers : dict Requested drivers and associated starter indexed by id. missing : list List of ids which do not correspond to any known valid driver. """ ds = self._drivers.contributions knowns = {d_id: ds[d_id] for d_id in drivers if d_id in ds} missing = list(set(drivers) - set(knowns)) return { d_id: (infos.cls, self._starters.contributions[infos.starter].starter) for d_id, infos in knowns.items() }, missing def get_profiles(self, user_id, profiles, try_release=True, partial=False): """Query profiles for use by a declared user. Parameters ---------- user_id : unicode Id of the user which request the authorization to use the instrument. profiles : list Ids of the instrument profiles which are requested. try_release : bool, optional Should we attempt to release currently used profiles. partial : bool, optional Should only a subset of the requested profiles be returned if some profiles are not available. Returns ------- profiles : dict Requested profiles as a dictionary. unavailable : list List of profiles that are not currently available and cannot be released. """ if user_id not in self.users: raise ValueError('Unknown instrument user tried to query profiles') used = [p for p in profiles if p in self.used_profiles] unavailable = [] if used: released = [] if not try_release: unavailable = used else: used_by_owner = defaultdict(set) for p in used: used_by_owner[self.used_profiles[p]].add(p) for o in list(used_by_owner): user = self._users.contributions[o] if user.policy == 'releasable': to_release = used_by_owner[o] r = user.release_profiles(self.workbench, to_release) unavailable.extend(set(to_release) - set(r)) released.extend(r) else: unavailable.extend(used_by_owner[o]) if unavailable and not partial: if released: used = { k: v for k, v in self.used_profiles.items() if k not in released } self.used_profiles = used return {}, unavailable available = ([p for p in profiles if p not in unavailable] if unavailable else profiles) with self.suppress_notifications(): u = self.used_profiles self.used_profiles = {} u.update({p: user_id for p in available}) self.used_profiles = u queried = {} for p in available: queried[p] = self._profiles[p]._config.dict() return queried, unavailable def release_profiles(self, user_id, profiles): """Release some previously acquired profiles. The user should not maintain any communication with the instruments whose profiles have been released after calling this method. Parameters ---------- user_id : unicode Id of the user releasing the profiles. profiles : iterable Profiles (ids) which are no longer needed by the user. """ self.used_profiles = { k: v for k, v in self.used_profiles.items() if k not in profiles or v != user_id } def get_aliases(self, manufacturer): """List the known aliases of a manufacturer. Parameters ---------- manufacturer : str Name of the manufacturer for which to return the aliases. Returns ------- aliases : list[unicode] Known aliases of the manufacturer. """ aliases = self._aliases.contributions.get(manufacturer, []) if aliases: aliases = aliases.aliases return aliases # ========================================================================= # --- Private API --------------------------------------------------------- # ========================================================================= #: Collector of drivers. _drivers = Typed(DeclaratorsCollector) #: Collector for the manufacturer aliases. _aliases = Typed(ExtensionsCollector) #: Declared manufacturers storing the corresponding model infos. _manufacturers = Typed(ManufacturersHolder) #: Collector of users. _users = Typed(ExtensionsCollector) #: Collector of starters. _starters = Typed(ExtensionsCollector) #: Collector of connections. _connections = Typed(ExtensionsCollector) #: Collector of settings. _settings = Typed(ExtensionsCollector) #: List of folders in which to search for profiles. # TODO make that list editable and part of the preferences _profiles_folders = List() #: Mapping of profile name to profile infos. _profiles = Dict() #: Watchdog observer tracking changes to the profiles folders. _observer = Typed(Observer) def _update_contribs(self, name, change): """Update the list of available contributions (editors, engines, tools) when they change. """ setattr(self, name, list(getattr(self, '_' + name).contributions)) if name == 'starters': for id_, s in getattr(self, '_' + name).contributions.items(): s.starter.id = id_ def _refresh_profiles(self): """List of profiles living in the profiles folders. """ profiles = {} logger = logging.getLogger(__name__) for path in self._profiles_folders: if os.path.isdir(path): filenames = sorted(f for f in os.listdir(path) if f.endswith('.instr.ini') and ( os.path.isfile(os.path.join(path, f)))) for filename in filenames: profile_path = os.path.join(path, filename) # Beware redundant names are overwritten name = filename[:-len('.instr.ini')] # TODO should be delayed and lead to a nicer report i = ProfileInfos(path=profile_path, plugin=self) res, msg = validate_profile_infos(i) if res: profiles[name] = i else: logger.warn(msg) else: logger.warn('{} is not a valid directory'.format(path)) self._profiles = profiles def _bind_observers(self): """Start the observers. """ for contrib in ('users', 'starters', 'connections', 'settings'): callback = partial(self._update_contribs, contrib) getattr(self, '_' + contrib).observe('contributions', callback) def update(): """Run the handler on the main thread to avoid GUI issues. """ deferred_call(self._refresh_profiles) self._observer = Observer() for folder in self._profiles_folders: handler = SystematicFileUpdater(update) self._observer.schedule(handler, folder, recursive=True) self._observer.start() def _unbind_observers(self): """Stop the observers. """ for contrib in ('users', 'starters', 'connections', 'settings'): callback = partial(self._update_contribs, contrib) getattr(self, '_' + contrib).observe('contributions', callback) self._observer.unschedule_all() self._observer.stop() try: self._observer.join() except RuntimeError: pass def _post_setattr__profiles(self, old, new): """Automatically update the profiles member. """ self.profiles = sorted(new) def _default__manufacturers(self): """Delayed till this is first needed. """ holder = ManufacturersHolder(plugin=self) valid_drivers = [d for d in self._drivers.contributions.values()] holder.update_manufacturers(valid_drivers) return holder
class NodeItem(GraphicsItem): """ A node-item in a node graph """ id = d_(Str()) name = d_(Unicode()) width = d_(Int(180)) height = d_(Int()) position = d_(Typed(Point2D)) edge_size = d_(Float(10.0)) title_height = d_(Float(24.0)) padding = d_(Float(4.0)) color_default = d_(ColorMember("#0000007F")) color_selected = d_(ColorMember("#FFA637FF")) font_title = d_(FontMember('10pt Ubuntu')) color_title = d_(ColorMember("#AAAAAAFF")) color_title_background = d_(ColorMember("#313131FF")) color_background = d_(ColorMember("#212121E3")) show_content_inline = d_(Bool(False)) #: the model item from the underlying graph structure model = d_(Typed(Atom)) context_menu_event = d_(Event()) recompute_node_layout = d_(Event()) #: optional Node Content content = Instance(NodeContent) input_sockets = List(NodeSocket) output_sockets = List(NodeSocket) input_sockets_visible = Property(lambda self: [s for s in self.input_sockets if s.visible], cached=True) output_sockets_visible = Property(lambda self: [s for s in self.output_sockets if s.visible], cached=True) input_sockets_dict = Property(lambda self: self._mk_input_dict(), cached=True) output_sockets_dict = Property(lambda self: self._mk_output_dict(), cached=True) #: Cyclic notification guard. This a bitfield of multiple guards. _guard = Int(0) #: A reference to the ProxyComboBox object. proxy = Typed(ProxyNodeItem) def _default_position(self): return Point2D(x=0, y=0) def _default_height(self): return self.compute_height() def _default_id(self): if self.scene is not None: cls = self.__class__ return self.scene.generate_item_id(cls.__name__, cls) return "<undefined>" def _default_name(self): return self.id #-------------------------------------------------------------------------- # Content Handlers #-------------------------------------------------------------------------- def child_added(self, child): """ Reset the item cache when a child is added """ super(NodeItem, self).child_added(child) if isinstance(child, NodeContent): self.content = child if isinstance(child, NodeSocket): if child.socket_type == SocketType.INPUT: self.input_sockets.append(child) self.get_member('input_sockets_dict').reset(self) elif child.socket_type == SocketType.OUTPUT: self.output_sockets.append(child) self.get_member('output_sockets_dict').reset(self) def child_removed(self, child): """ Reset the item cache when a child is removed """ super(NodeItem, self).child_removed(child) if isinstance(child, NodeContent): self.content = None if isinstance(child, NodeSocket): if child.socket_type == SocketType.INPUT: self.input_sockets.remove(child) self.get_member('input_sockets_dict').reset(self) elif child.socket_type == SocketType.OUTPUT: self.output_sockets.remove(child) self.get_member('output_sockets_dict').reset(self) def _mk_input_dict(self): return {c.id: c for c in self.children if isinstance(c, NodeSocket) and c.socket_type == SocketType.INPUT} def _mk_output_dict(self): return {c.id: c for c in self.children if isinstance(c, NodeSocket) and c.socket_type == SocketType.OUTPUT} def activate_bottom_up(self): self.assign_socket_indices() #-------------------------------------------------------------------------- # Observers #-------------------------------------------------------------------------- @observe('id', 'name', 'width', 'height', 'edge_size', 'title_height', 'padding', 'color_default', 'color_selected', 'color_title', 'color_title_background', 'color_background', 'show_content_inline', 'content') def _update_proxy(self, change): """ An observer which sends state change to the proxy. """ # The superclass handler implementation is sufficient. super(NodeItem, self)._update_proxy(change) self.request_update() @observe('width', 'height', 'edge_size', 'title_height', 'padding') def _update_layout(self, change): for s in self.input_sockets + self.output_sockets: s.update_sockets() if self.content is not None: self.content.update_content_geometry() def _observe_recompute_node_layout(self, change): if self.initialized: if not self._guard & NODE_UPDATE_LAYOUT_GUARD: self.update_node_layout() #-------------------------------------------------------------------------- # NodeItem API #-------------------------------------------------------------------------- def update_node_layout(self): self._guard |= NODE_UPDATE_LAYOUT_GUARD self.get_member('input_sockets_visible').reset(self) self.get_member('output_sockets_visible').reset(self) self.assign_socket_indices() self.height = self.compute_height() self.update_sockets_and_edges() self._guard &= ~NODE_UPDATE_LAYOUT_GUARD def assign_socket_indices(self): for socket in self.input_sockets: if socket in self.input_sockets_visible: socket.index = self.input_sockets_visible.index(socket) else: socket.index = 0 for socket in self.output_sockets: if socket in self.output_sockets_visible: socket.index = self.output_sockets_visible.index(socket) else: socket.index = 0 def update_sockets_and_edges(self): for socket in self.input_sockets_visible + self.output_sockets_visible: socket.update_sockets() for edge in socket.edges: edge.update_positions() def compute_height(self): socket_space = max(sum(s.socket_spacing for s in self.input_sockets_visible), sum(s.socket_spacing for s in self.output_sockets_visible)) return math.ceil(self.title_height + 2 * self.padding + 2 * self.edge_size + socket_space) # XXX must avoid cyclic updates .. def set_position(self, pos): self.position = pos self.proxy.set_position(pos) def getContentView(self): if not self.show_content_inline and self.content is not None: return self.content.content_objects[:] return []
class TextMonitor(BaseMonitor): """ Simple monitor displaying entries as text in a widget. """ # --- Public API ---------------------------------------------------------- #: List of the entries which should be displayed when a measure is running. displayed_entries = ContainerList(Instance(MonitoredEntry)) #: List of the entries which should not be displayed when a measure is #: running. undisplayed_entries = ContainerList(Instance(MonitoredEntry)) #: List of the entries which should be not displayed when a measure is #: running because they would be redundant with another entry. (created by #: a rule for example.) hidden_entries = List(Instance(MonitoredEntry)) #: Mapping between a database entry and a list of callable used for #: updating an entry of the monitor which relies on the database entry. updaters = Dict(Str(), List(Callable())) #: List of rules which should be used to build monitor entries. rules = ContainerList(Instance(AbstractMonitorRule)) #: List of user created monitor entries. custom_entries = List(Instance(MonitoredEntry)) def start(self, parent_ui): if self.auto_show: self.show_monitor(parent_ui) def stop(self): # Avoid raising errors if the view has already been destroyed. if getattr(self._view, 'proxy_is_active', None): self._view.close() self._view = None def process_news(self, news): values = self._database_values values[news[0]] = news[1] for updater in self.updaters[news[0]]: updater(values) def refresh_monitored_entries(self, entries={}): if not entries: entries = self._database_values else: self._database_values = entries custom = self.custom_entries[:] self.clear_state() self.custom_entries = custom for entry, value in entries.iteritems(): self.database_modified({'value': (entry, value)}) def database_modified(self, change): entry = change['value'] # Handle the addition of a new entry to the database if len(entry) > 1: # Store the new value. self._database_values[entry[0]] = entry[1] # Add a default entry to the displayed monitor entries. self.displayed_entries.append(self._create_default_entry(*entry)) # Try to apply rules. for rule in self.rules: rule.try_apply(entry[0], self) # Check whether any custom entry is currently hidden. hidden_custom = [ e for e in self.custom_entries if e not in self.displayed_entries or e not in self.undisplayed_entries ] # If there is one checks whether all the dependences are once # more available. if hidden_custom: for e in hidden_custom: if all(d in self.database_entries for d in e.depend_on): self.displayed_entries.append(e) # Handle the case of a database entry being suppressed, by removing all # monitors entries which where depending on this entry. else: self.displayed_entries[:] = [ m for m in self.displayed_entries if entry[0] not in m.depend_on ] self.undisplayed_entries[:] = [ m for m in self.undisplayed_entries if entry[0] not in m.depend_on ] self.hidden_entries[:] = [ m for m in self.hidden_entries if entry[0] not in m.depend_on ] if entry[0] in self.database_entries: self.database_entries.remove(entry[0]) if entry[0] in self._database_values: del self._database_values[entry[0]] def clear_state(self): """ Clear the monitor state. """ with self.suppress_notifications(): self.displayed_entries = [] self.undisplayed_entries = [] self.hidden_entries = [] self.updaters = {} self.custom_entries = [] self.database_entries = [] def get_state(self): prefs = self.preferences_from_members() # Save the definitions of the custom entries. for i, custom_entry in enumerate(self.custom_entries): aux = 'custom_{}'.format(i) prefs[aux] = custom_entry.preferences_from_members() # Save the definitions of the rules. for i, rule in enumerate(self.rules): aux = 'rule_{}'.format(i) prefs[aux] = rule.preferences_from_members() prefs['displayed'] = repr([e.path for e in self.displayed_entries]) prefs['undisplayed'] = repr([e.path for e in self.undisplayed_entries]) prefs['hidden'] = repr([e.path for e in self.hidden_entries]) return prefs def set_state(self, config, entries): # Request all the rules class from the plugin. rules_config = [ conf for name, conf in config.iteritems() if name.startswith('rule_') ] # Rebuild all rules. rules = [] for rule_config in rules_config: rule = self._plugin.build_rule(rule_config) if rule is not None: rules.append(rule) self.rules = rules customs_config = [ conf for name, conf in config.iteritems() if name.startswith('custom_') ] for custom_config in customs_config: entry = MonitoredEntry() entry.update_members_from_preferences(**custom_config) self.custom_entries.append(entry) self.refresh_monitored_entries(entries) m_entries = set(self.displayed_entries + self.undisplayed_entries + self.hidden_entries + self.custom_entries) pref_disp = eval(config['displayed']) pref_undisp = eval(config['undisplayed']) pref_hidden = eval(config['hidden']) disp = [e for e in m_entries if e.path in pref_disp] m_entries -= set(disp) undisp = [e for e in m_entries if e.path in pref_undisp] m_entries -= set(undisp) hidden = [e for e in m_entries if e.path in pref_hidden] m_entries -= set(hidden) if m_entries: e_l = [e.name for e in m_entries] mess = cleandoc('''The following entries were not expected from the config : {} . These entries has been added to the displayed ones.''') information(parent=None, title='Unhandled entries', text=fill(mess.format(e_l))) pref_disp += list(m_entries) self.displayed_entries = disp self.undisplayed_entries = undisp self.hidden_entries = hidden self.measure_name = config['measure_name'] self.auto_show = eval(config['auto_show']) def get_editor_page(self): return TextMonitorPage(monitor=self) def show_monitor(self, parent_ui): if self._view and self._view.proxy_is_active: self._view.restore() self._view.send_to_front() else: view = TextMonitorView(monitor=self) view.show() self._view = view @property def all_database_entries(self): """ Getter returning all known database entries. """ return self._database_values.keys() def add_rule_to_plugin(self, rule_name): """ Add a rule definition to the plugin. Parameters ---------- rule_name : str Name of the rule whose description should be added to the plugin. """ plugin = self._plugin if rule_name in self._plugin.rules: return config = {} for rule in self.rules: if rule.name == rule_name: config = rule.preferences_from_members() break if config: plugin.rules[rule_name] = config # --- Private API --------------------------------------------------------- # Known values of the database entries used when recomputing an entry value # depending not on a single value. During edition all values are stored, # regardless of whether or not the entry needs to be observed, when the # start method is called the dict is cleaned. _database_values = Dict(Str(), Value()) # Reference to the monitor plugin handling the rules persistence. _plugin = ForwardTyped(import_monitor_plugin) # Reference to the current display _view = Typed(TextMonitorView) @staticmethod def _create_default_entry(entry_path, value): """ Create a monitor entry for a database entry. Parameters ---------- entry_path : str Path of the database entries for which to create a monitor entry. Returns ------- entry : MonitoredEntry Monitor entry to be added to the monitor. """ name = entry_path.rsplit('/', 1)[-1] formatting = '{' + entry_path + '}' entry = MonitoredEntry(name=name, path=entry_path, formatting=formatting, depend_on=[entry_path]) entry.value = '{}'.format(value) return entry def _observe_displayed_entries(self, change): """ Observer updating internals when the displayed entries change. This observer ensure that the list of database entries which need to be monitored reflects the actual needs of the monitor and that the monitor entries updaters mapping is up to date. """ if change['type'] == 'update': added = set(change['value']) - set(change['oldvalue']) removed = set(change['oldvalue']) - set(change['value']) for entry in removed: self._displayed_entry_removed(entry) for entry in added: self._displayed_entry_added(entry) elif change['type'] == 'container': op = change['operation'] if op in ('__iadd__', 'append', 'extend', 'insert'): if 'item' in change: self._displayed_entry_added(change['item']) if 'items' in change: for entry in change['items']: self._displayed_entry_added(entry) elif op in ('__delitem__', 'remove', 'pop'): if 'item' in change: self._displayed_entry_removed(change['item']) if 'items' in change: for entry in change['items']: self._displayed_entry_removed(entry) elif op in '__setitem__': old = change['olditem'] if isinstance(old, list): for entry in old: self._displayed_entry_removed(entry) else: self._displayed_entry_removed(old) new = change['newitem'] if isinstance(new, list): for entry in new: self._displayed_entry_added(entry) else: self._displayed_entry_added(new) def _displayed_entry_added(self, entry): """ Tackle the addition of a displayed monitor entry. First this method will add the entry updater into the updaters dict for each of its dependence and if one dependence is absent from the database_entries it will be added. Parameters ---------- entry : MonitoredEntry The entry being added to the list of displayed entries of the monitor. """ for dependence in entry.depend_on: if dependence in self.updaters: self.updaters[dependence].append(entry.update) else: self.updaters[dependence] = [entry.update] if dependence not in self.database_entries: self.database_entries.append(dependence) def _displayed_entry_removed(self, entry): """ Tackle the deletion of a displayed monitor entry. First this method will remove the entry updater for each of its dependence and no updater remain for that database entry, the entry will be removed from the database_entries Parameters ---------- entry : MonitoredEntry The entry being added to the list of displayed entries of the monitor. """ for dependence in entry.depend_on: self.updaters[dependence].remove(entry.update) if not self.updaters[dependence]: del self.updaters[dependence] self.database_entries.remove(dependence)
class JDF_Top(SubAgent): """Top class that controls distribution of patterns into JDF""" base_name="jdf" def show(self): shower(self, self.wafer_coords) def gen_jdf(self, agents): self.clear_JDF() for n, p in enumerate(agents): if p.plot_sep: self.patterns.append(JDF_Pattern(num=n+1, name=p.name)) self.sub_arrays.append(JDF_Array(array_num=n+1, assigns=[JDF_Assign(assign_type=["P({0})".format(n+1)], short_name=p.name, pos_assign=[(1, 1)])])) self.main_arrays[0].assigns.append(JDF_Assign(assign_type=["A({0})".format(n+1)], short_name=p.name, pos_assign=[(n+1, 1)])) self.input_jdf=self.jdf_produce() wafer_coords=FullWafer(name="jdf_wafer_coords") plot=Plotter(name="jdf_plot") @private_property def xy_offsets(self): """recursive traces down locations of all patterns""" overall_dict={} for pd in [self.p_off_recur(m_arr, p_off={}) for m_arr in self.main_arrays]: for key in pd: overall_dict[key]=overall_dict.get(key, [])+pd[key] return overall_dict def p_off_recur(self, p_arr, x_off_in=0, y_off_in=0, p_off={}): """recursive search function for pattern locations""" for a in p_arr.assigns: for pa in a.pos_assign: x_off=x_off_in+p_arr.x_start+(pa[0]-1)*p_arr.x_step y_off=y_off_in+p_arr.y_start-(pa[1]-1)*p_arr.y_step for p in [pattern for pattern in self.patterns if pattern.num in a.P_nums]: if p.name not in p_off: p_off[p.name]=[] p_off[p.name].append((x_off+p.x, y_off+p.y)) for arr in [array for array in self.sub_arrays if array.array_num in a.A_nums]: self.p_off_recur(arr, x_off, y_off, p_off) return p_off def assign_condition(self, item, n=0): return [assign.short_name for assign in self.main_arrays[n].assigns if item in assign.pos_assign] distribute_event=Event() def _observe_distribute_event(self, change): self.distribute_coords() self.get_member("xy_offsets").reset(self) xmin=-0.05 xmax=0.05 ymin=-0.05 ymax=0.05 self.plot.axe.texts=[] for main_arr in self.main_arrays: for asgn in main_arr.assigns: for pos_asgn in asgn.pos_assign: x_off=main_arr.x_start+(pos_asgn[0]-1)*main_arr.x_step y_off=main_arr.y_start-(pos_asgn[1]-1)*main_arr.y_step self.plot.add_text(asgn.short_name, x_off, y_off, size=4.5, alpha=0.8, ha='center') xmin=min(xmin, x_off) xmax=max(xmax, x_off) ymin=min(ymin, y_off) ymax=max(ymax, y_off) STRETCH=self.wafer_coords.gap_size self.plot.set_xlim(xmin-STRETCH, xmax+STRETCH) self.plot.set_ylim(ymin-STRETCH, ymax+STRETCH) self.plot.xlabel="x (um)" self.plot.ylabel="y (um)" self.plot.title="JDF Pattern Distribution" self.plot.draw() @private_property def view_window(self): from enaml import imports with imports(): from taref.ebl.jdf_e import JDF_View return JDF_View(jdf=self) def append_valcom(self, inlist, name, fmt_str="{0}{1}", sep=";"): comment=format_comment(get_tag(self, name, "comment", "")) value=getattr(self, name) inlist.append(fmt_str.format(value, comment)) @property def arrays(self): return sqze(self.main_arrays, self.sub_arrays) def distribute_coords(self, num=None): """distribute coords using wafer_coords object""" for qw in self.wafer_coords.quarter_wafers: qw.get_member('bad_coords').reset(qw) qw.get_member('good_coords').reset(qw) self.comments=["distributed main array for quarter wafer {}".format(self.wafer_coords.wafer_type)] self.Px, self.Py, self.Qx, self.Qy=self.wafer_coords.GLM if self.wafer_coords.wafer_type=="Full" and len(self.main_arrays)<4: assigns=[assign.dup_assign() for assign in self.main_arrays[0].assigns] self.main_arrays=self._default_main_arrays() for arr in self.main_arrays: arr.assigns=[assign.dup_assign() for assign in assigns] for m, qw in enumerate(self.wafer_coords.quarter_wafers): if num is None: our_num=len(self.main_arrays[m].assigns) else: our_num=num for n, c in enumerate(qw.distribute_coords(our_num)): self.main_arrays[m].assigns[n].pos_assign=c[:] self.main_arrays[m].x_start=qw.x_offset self.main_arrays[m].x_num=qw.N_chips self.main_arrays[m].x_step=qw.step_size self.main_arrays[m].y_start=qw.y_offset self.main_arrays[m].y_num=qw.N_chips self.main_arrays[m].y_step=qw.step_size self.output_jdf=self.jdf_produce() self.input_jdf=self.output_jdf input_jdf=Unicode() output_jdf=Unicode() comments=List() Px=Coerced(int, (-40000,)).tag(desc="X coordinate of global P mark") Py=Coerced(int, (4000,)).tag(desc="Y coordinate of global P mark") Qx=Coerced(int, (-4000,)) Qy=Coerced(int, (40000,)) mgn_name=Unicode("IDT") wafer_diameter=Coerced(int, (4,)) write_diameter=Coerced(float, (-4.2,)) stdcur=Coerced(int, (2,)).tag(desc="Current to use in nA") shot=Coerced(int, (8,)).tag(desc="Shot size in nm. should divide 4 um evenly") resist=Coerced(int, (165,)).tag(desc="dose") resist_comment=Unicode() main_arrays=List().tag(desc="main arrays in JDF", private=True) sub_arrays=List().tag(desc="arrays in JDF")#.tag(width='max', inside_type=jdf_array) patterns=List().tag(desc="patterns in JDF")#.tag(width='max', inside_type=jdf_pattern) jdis=List().tag(desc="jdis in JDF") def _default_main_arrays(self): if self.wafer_coords.wafer_type=="Full": return [JDF_Main_Array(), JDF_Main_Array(), JDF_Main_Array(), JDF_Main_Array()] return [JDF_Main_Array()] def _observe_input_jdf(self, change): self.jdf_parse(self.input_jdf) self.output_jdf=self.jdf_produce() def clear_JDF(self): self.comments=[] self.main_arrays=self._default_main_arrays() self.sub_arrays=[] self.patterns=[] self.jdis=[] def add_pattern(self, tempstr, comment): self.patterns.append(JDF_Pattern(tempstr=tempstr, comment=comment)) def add_array(self, tempstr, comment): if ":" in tempstr: array=JDF_Array(tempstr=tempstr, comment=comment) self.sub_arrays.append(array) else: array=JDF_Main_Array(tempstr=tempstr, comment=comment) self.main_arrays.append(array) return array def jdf_parse(self, jdf_data): """reads a jdf text and puts the data into objects""" jdf_list=jdf_data.split("\n") inside_path=False inside_layer=False self.clear_JDF() for n, line in enumerate(jdf_list): tempstr, comment=parse_comment(line) if tempstr=="" and comment!="": self.comments.append(comment) if tempstr.startswith('GLMPOS'): self.Px, self.Py, self.Qx, self.Qy=xy_string_split(tempstr) elif tempstr.startswith('JOB'): mgn_name, self.wafer_diameter, self.write_diameter=tempstr.split(",") self.mgn_name=mgn_name.split("'")[1].strip() elif tempstr.startswith("PATH"): inside_path=True elif "LAYER" in tempstr: inside_layer=True if inside_path: if 'ARRAY' in tempstr: array=self.add_array(tempstr, comment) elif 'ASSIGN' in tempstr: array.add_assign(tempstr, comment) elif 'CHMPOS' in tempstr: M1x, M1y=tuple_split(tempstr) if len(self.main_arrays)>0: self.main_arrays[-1].M1x=M1x self.main_arrays[-1].M1y=M1y elif "PEND" in tempstr: inside_path=False elif inside_layer: if 'END' in tempstr: inside_layer=False elif 'STDCUR' in tempstr: set_attr(self, "stdcur", tempstr.split("STDCUR")[1], comment=comment) elif 'SHOT' in tempstr: set_attr(self, "shot", tempstr.split(',')[1], comment=comment) elif 'RESIST' in tempstr: set_attr(self, "resist", tempstr.split('RESIST')[1], comment=comment) elif 'P(' in tempstr: self.add_pattern(tempstr, comment) elif tempstr.startswith('@'): jdi_str=tempstr.split("'")[1].split(".jdi")[0] self.jdis.append(jdi_str) def jdf_produce(self): """produces a jdf from the data stored in the object""" jl=[] jl.append("JOB/W '{name}', {waf_diam}, {write_diam}\n".format(name=self.mgn_name, waf_diam=self.wafer_diameter, write_diam=self.write_diameter)) if len(self.comments)>0: jl.append(";{comment}\n".format(comment=self.comments[0])) jl.append("GLMPOS P=({Px}, {Py}), Q=({Qx},{Qy})".format(Px=self.Px, Py=self.Py, Qx=self.Qx, Qy=self.Qy)) jl.append("PATH") for n, item in enumerate(self.arrays): jl.extend(item.jdf_output) jl.append("PEND\n\nLAYER 1") for n, item in enumerate(self.patterns): jl.append(item.jdf_output) self.append_valcom(jl, "stdcur", "\nSTDCUR {0}{1}") self.append_valcom(jl, "shot", "SHOT A, {0}{1}") self.append_valcom(jl, "resist", "RESIST {0}{1}\n") for item in self.jdis: jl.append("@ '{jdi_name}.jdi'".format(jdi_name=item)) jl.append("\nEND 1\n") if len(self.comments)>1: for item in self.comments[1:]: jl.append(";{}".format(item)) return "\n".join(jl)
class KeysResponse(Response): """ Expected response from api/keys """ Keys = List(Key) RecipientType = Int()
class ComplexTask(BaseTask): """Task composed of several subtasks. """ #: List of all the children of the task. The list should not be manipulated #: directly by user code. #: The tag 'child' is used to mark that a member can contain child tasks #: and is used to gather children for operation which must occur on all of #: them. children = List().tag(child=100) #: Signal emitted when the list of children change, the payload will be a #: ContainerChange instance. #: The tag 'child_notifier' is used to mark that a member emmit #: notifications about modification of another 'child' member. This allow #: editors to correctly track all of those. children_changed = Signal().tag(child_notifier='children') def perform(self): """Run sequentially all child tasks. """ for child in self.children: child.perform_() def check(self, *args, **kwargs): """Run test of all child tasks. """ test, traceback = super(ComplexTask, self).check(*args, **kwargs) for child in self.gather_children(): try: check = child.check(*args, **kwargs) test = test and check[0] traceback.update(check[1]) except Exception: test = False msg = 'An exception occured while running check :\n%s' traceback[child.path + '/' + child.name] = msg % format_exc() return test, traceback def prepare(self): """Overridden to prepare also children tasks. """ super(ComplexTask, self).prepare() for child in self.gather_children(): child.prepare() def add_child_task(self, index, child): """Add a child task at the given index. Parameters ---------- index : int Index at which to insert the new child task. child : BaseTask Task to insert in the list of children task. """ self.children.insert(index, child) # In the absence of a root task do nothing else than inserting the # child. if self.root is not None: child.depth = self.depth + 1 child.database = self.database child.path = self._child_path() # Give him its root so that it can proceed to any child # registration it needs to. child.parent = self child.root = self.root # Ask the child to register in database child.register_in_database() # Register anew preferences to keep the right ordering for the # children self.register_preferences() change = ContainerChange(obj=self, name='children', added=[(index, child)]) self.children_changed(change) def move_child_task(self, old, new): """Move a child task. Parameters ---------- old : int Index at which the child to move is currently located. new : BaseTask Index at which to insert the child task. """ child = self.children.pop(old) self.children.insert(new, child) # In the absence of a root task do nothing else than moving the # child. if self.root is not None: # Register anew preferences to keep the right ordering for the # children self.register_preferences() change = ContainerChange(obj=self, name='children', moved=[(old, new, child)]) self.children_changed(change) def remove_child_task(self, index): """Remove a child task from the children list. Parameters ---------- index : int Index at which the child to remove is located. """ child = self.children.pop(index) # Cleanup database, update preferences child.unregister_from_database() child.root = None child.parent = None child.database = None self.register_preferences() change = ContainerChange(obj=self, name='children', removed=[(index, child)]) self.children_changed(change) def gather_children(self): """Build a flat list of all children task. Children tasks are ordered according to their 'child' tag value. Returns ------- children : list List of all the task children. """ children = [] tagged = tagged_members(self, 'child') for name in sorted(tagged, key=lambda m: tagged[m].metadata['child']): child = getattr(self, name) if child: if isinstance(child, Iterable): children.extend(child) else: children.append(child) return children def traverse(self, depth=-1): """Reimplemented to yield all child task. """ yield self if depth == 0: for c in self.gather_children(): if c: yield c else: for c in self.gather_children(): if c: for subc in c.traverse(depth - 1): yield subc def register_in_database(self): """Create a node in the database and register all entries. This method registers both the task entries and all the tasks tagged as child. """ super(ComplexTask, self).register_in_database() self.database.create_node(self.path, self.name) # ComplexTask defines children so we always get something for child in self.gather_children(): child.register_in_database() def unregister_from_database(self): """Unregister all entries and delete associated database node. This method unregisters both the task entries and all the tasks tagged as child. """ super(ComplexTask, self).unregister_from_database() for child in self.gather_children(): child.unregister_from_database() self.database.delete_node(self.path, self.name) def register_preferences(self): """Register the task preferences into the preferences system. This method registers both the task preferences and all the preferences of the tasks tagged as child. """ self.preferences.clear() for name, member in tagged_members(self, 'pref').items(): # Register preferences. val = getattr(self, name) self.preferences[name] = member_to_pref(self, member, val) # Find all tagged children. for name in tagged_members(self, 'child'): child = getattr(self, name) if child: if isinstance(child, Iterable): for i, aux in enumerate(child): child_id = name + '_{}'.format(i) self.preferences[child_id] = {} aux.preferences = \ self.preferences[child_id] aux.register_preferences() else: self.preferences[name] = {} child.preferences = self.preferences[name] child.register_preferences() def update_preferences_from_members(self): """Update the values stored in the preference system. This method updates both the task preferences and all the preferences of the tasks tagged as child. """ for name, member in tagged_members(self, 'pref').items(): val = getattr(self, name) self.preferences[name] = member_to_pref(self, member, val) for child in self.gather_children(): child.update_preferences_from_members() @classmethod def build_from_config(cls, config, dependencies): """Create a new instance using the provided infos for initialisation. Parameters ---------- config : dict(str) Dictionary holding the new values to give to the members in string format, or dictionnary like for instance with prefs. dependencies : dict Dictionary holding the necessary classes needed when rebuilding. This is assembled by the TaskManager. Returns ------- task : BaseTask Newly created and initiliazed task. Notes ----- This method is fairly powerful and can handle a lot of cases so don't override it without checking that it works. """ task = cls() update_members_from_preferences(task, config) for name, member in tagged_members(task, 'child').items(): if isinstance(member, List): i = 0 pref = name + '_{}' validated = [] while True: child_name = pref.format(i) if child_name not in config: break child_config = config[child_name] child_class_name = child_config.pop('task_id') child_cls = dependencies[DEP_TYPE][child_class_name] child = child_cls.build_from_config(child_config, dependencies) validated.append(child) i += 1 else: if name not in config: continue child_config = config[name] child_class_name = child_config.pop('task_id') child_class = dependencies[DEP_TYPE][child_class_name] validated = child_class.build_from_config(child_config, dependencies) setattr(task, name, validated) return task # ========================================================================= # --- Private API --------------------------------------------------------- # ========================================================================= #: Last removed child and list of database access exceptions attached to #: it and necessity to observe its _access_exs. _last_removed = Tuple(default=(None, None, False)) #: Last access exceptions desactivated from a child. _last_exs = Coerced(set) #: List of access_exs, linked to access exs in child, disabled because #: child disabled some access_exs. _disabled_exs = List() def _child_path(self): """Convenience function returning the path to set for child task. """ return self.path + '/' + self.name def _update_children_path(self): """Update the path of all children. """ for child in self.gather_children(): child.path = self._child_path() if isinstance(child, ComplexTask): child._update_children_path() def _post_setattr_name(self, old, new): """Handle the task being renamed at runtime. If the task is renamed at runtime, it means that the path of all the children task is now obsolete and that the database node of this task must be renamed (database handles the exception. """ if old and self.database: super(ComplexTask, self)._post_setattr_name(old, new) self.database.rename_node(self.path, old, new) # Update the path of all children. self._update_children_path() def _post_setattr_root(self, old, new): """Make sure that all children get all the info they need to behave correctly when the task get its root parent (ie the task is now in a 'correct' environnement). """ if new is None: self.database = None for child in self.gather_children(): child.root = None child.database = None return for child in self.gather_children(): child.depth = self.depth + 1 child.database = self.database child.path = self._child_path() # Give him its root so that it can proceed to any child # registration it needs to. child.parent = self child.root = self.root
class AddressesResponse(Response): """ Expected response from api/addresses """ Addresses = List(Address)
class MessageReadResponse(Response): """ Expected response from put api/messages/read """ Responses = List(MessageReadResult)
class ContactsResponse(Response): """ Expected response from api/contacts """ Limit = Int() Total = Int() Contacts = List(Contact)
class ConversationResponse(Response): """ Expected response from api/conversations/<id> """ Conversation = Instance(Conversation) Messages = List(Message)
class ConversationsResponse(Response): """ Expected response from api/conversations """ Limit = Int() Total = Int() Conversations = List(Conversation)
class QtOccViewer(QtControl, ProxyOccViewer): #: Viewer widget widget = Typed(QtViewer3d) #: Update count _redraw_blocked = Bool() #: Displayed Shapes _displayed_shapes = Dict() _displayed_dimensions = Dict() _displayed_graphics = Dict() _selected_shapes = List() #: Errors errors = Dict() #: Tuple of (Quantity_Color, transparency) shape_color = Typed(tuple) #: Grid colors grid_colors = Dict() #: Shapes shapes = Property(lambda self: self.get_shapes(), cached=True) #: Dimensions dimensions = Typed(set) graphics = Typed(set) # ------------------------------------------------------------------------- # OpenCascade specific members # ------------------------------------------------------------------------- display_connection = Typed(Aspect_DisplayConnection) v3d_viewer = Typed(V3d_Viewer) v3d_view = Typed(V3d_View) ais_context = Typed(AIS_InteractiveContext) prs3d_drawer = Typed(Prs3d_Drawer) prs_mgr = Typed(PrsMgr_PresentationManager) v3d_window = Typed(V3d_Window) gfx_structure_manager = Typed(Graphic3d_StructureManager) gfx_structure = Typed(Graphic3d_Structure) graphics_driver = Typed(OpenGl_GraphicDriver) camera = Typed(Graphic3d_Camera) #: List of lights lights = List() #: Fired _redisplay_timer = Typed(QTimer, ()) _qt_app = Property(lambda self: Application.instance()._qapp, cached=True) def get_shapes(self): return [c for c in self.children() if not isinstance(c, QtControl)] def create_widget(self): self.widget = QtViewer3d(parent=self.parent_widget()) def init_widget(self): super().init_widget() widget = self.widget widget.proxy = self redisplay_timer = self._redisplay_timer redisplay_timer.setSingleShot(True) redisplay_timer.setInterval(8) redisplay_timer.timeout.connect(self.on_redisplay_requested) def init_viewer(self): """ Init viewer when the QOpenGLWidget is ready """ d = self.declaration widget = self.widget if sys.platform == 'win32': display = Aspect_DisplayConnection() else: display_name = TCollection_AsciiString( os.environ.get('DISPLAY', '0')) display = Aspect_DisplayConnection(display_name) self.display_connection = display # Create viewer graphics_driver = self.graphics_driver = OpenGl_GraphicDriver(display) viewer = self.v3d_viewer = V3d_Viewer(graphics_driver) view = self.v3d_view = viewer.CreateView() # Setup window win_id = widget.get_window_id() if sys.platform == 'win32': window = WNT_Window(win_id) elif sys.platform == 'darwin': window = Cocoa_Window(win_id) else: window = Xw_Window(self.display_connection, win_id) if not window.IsMapped(): window.Map() self.v3d_window = window view.SetWindow(window) view.MustBeResized() # Setup viewer ais_context = self.ais_context = AIS_InteractiveContext(viewer) drawer = self.prs3d_drawer = ais_context.DefaultDrawer() # Needed for displaying graphics prs_mgr = self.prs_mgr = ais_context.MainPrsMgr() gfx_mgr = self.gfx_structure_manager = prs_mgr.StructureManager() self.gfx_structure = Graphic3d_Structure(gfx_mgr) # Lights camera self.camera = view.Camera() try: self.set_lights(d.lights) except Exception as e: log.exception(e) viewer.SetDefaultLights() #viewer.DisplayPrivilegedPlane(True, 1) #view.SetShadingModel(Graphic3d_TypeOfShadingModel.V3d_PHONG) # background gradient with self.redraw_blocked(): self.set_background_gradient(d.background_gradient) self.set_draw_boundaries(d.draw_boundaries) self.set_trihedron_mode(d.trihedron_mode) self.set_display_mode(d.display_mode) self.set_hidden_line_removal(d.hidden_line_removal) self.set_selection_mode(d.selection_mode) self.set_view_mode(d.view_mode) self.set_view_projection(d.view_projection) self.set_lock_rotation(d.lock_rotation) self.set_lock_zoom(d.lock_zoom) self.set_shape_color(d.shape_color) self.set_chordial_deviation(d.chordial_deviation) self._update_rendering_params() self.set_grid_mode(d.grid_mode) self.set_grid_colors(d.grid_colors) self.init_signals() self.dump_gl_info() self.redraw() qt_app = self._qt_app for child in self.children(): self.child_added(child) qt_app.processEvents() def dump_gl_info(self): # Debug info try: ctx = self.graphics_driver.GetSharedContext() if ctx is None or not ctx.IsValid(): return v1 = ctx.VersionMajor() v2 = ctx.VersionMinor() log.info("OpenGL version: {}.{}".format(v1, v2)) log.info("GPU memory: {}".format(ctx.AvailableMemory())) log.info("GPU memory info: {}".format( ctx.MemoryInfo().ToCString())) log.info("Max MSAA samples: {}".format(ctx.MaxMsaaSamples())) supports_raytracing = ctx.HasRayTracing() log.info("Supports ray tracing: {}".format(supports_raytracing)) if supports_raytracing: log.info("Supports textures: {}".format( ctx.HasRayTracingTextures())) log.info("Supports adaptive sampling: {}".format( ctx.HasRayTracingAdaptiveSampling())) log.info("Supports adaptive sampling atomic: {}".format( ctx.HasRayTracingAdaptiveSamplingAtomic())) else: ver_too_low = ctx.IsGlGreaterEqual(3, 1) if not ver_too_low: log.info("OpenGL version must be >= 3.1") else: ext = "GL_ARB_texture_buffer_object_rgb32" if not ctx.CheckExtension(ext): log.info("OpenGL extension {} is missing".format(ext)) else: log.info("OpenGL glBlitFramebuffer is missing") except Exception as e: log.exception(e) def init_signals(self): d = self.declaration callbacks = self.widget._callbacks for name in callbacks.keys(): cb = getattr(d, name, None) if cb is not None: callbacks[name].append(cb) def child_added(self, child): if isinstance(child, OccShape): self._add_shape_to_display(child) elif isinstance(child, OccDimension): self._add_dimension_to_display(child) else: super().child_added(child) def child_removed(self, child): if isinstance(child, OccShape): self._remove_shape_from_display(child) elif isinstance(child, OccDimension): self._remove_dimension_from_display(child) else: super().child_removed(child) def _add_shape_to_display(self, occ_shape): """ Add an OccShape to the display """ d = occ_shape.declaration if not d.display: return displayed_shapes = self._displayed_shapes display = self.ais_context.Display qt_app = self._qt_app occ_shape.displayed = True for s in occ_shape.walk_shapes(): s.observe('ais_shape', self.on_ais_shape_changed) ais_shape = s.ais_shape if ais_shape is not None: try: display(ais_shape, False) s.displayed = True displayed_shapes[s.shape] = s except RuntimeError as e: log.exception(e) # Displaying can take a lot of time qt_app.processEvents() if isinstance(occ_shape, OccPart): for d in occ_shape.declaration.traverse(): proxy = getattr(d, 'proxy', None) if proxy is None: continue if isinstance(proxy, OccDimension): self._add_dimension_to_display(proxy) elif isinstance(proxy, OccDisplayItem): self._add_item_to_display(proxy) self._redisplay_timer.start() def _remove_shape_from_display(self, occ_shape): displayed_shapes = self._displayed_shapes remove = self.ais_context.Remove occ_shape.displayed = False for s in occ_shape.walk_shapes(): s.unobserve('ais_shape', self.on_ais_shape_changed) if s.get_member('ais_shape').get_slot(s) is None: continue ais_shape = s.ais_shape if ais_shape is not None: s.displayed = False displayed_shapes.pop(ais_shape, None) remove(ais_shape, False) if isinstance(occ_shape, OccPart): for d in occ_shape.declaration.traverse(): proxy = getattr(d, 'proxy', None) if proxy is None: continue if isinstance(proxy, OccDimension): self._remove_dimension_from_display(proxy) elif isinstance(proxy, OccDisplayItem): self._remove_item_from_display(proxy) self._redisplay_timer.start() def on_ais_shape_changed(self, change): ais_context = self.ais_context displayed_shapes = self._displayed_shapes occ_shape = change['object'] if change['type'] == 'update': old_ais_shape = change['oldvalue'] if old_ais_shape is not None: old_shape = old_ais_shape.Shape() displayed_shapes.pop(old_shape, None) ais_context.Remove(old_ais_shape, False) occ_shape.displayed = False new_ais_shape = change['value'] if new_ais_shape is not None: displayed_shapes[occ_shape.shape] = occ_shape ais_context.Display(new_ais_shape, False) occ_shape.displayed = True self._redisplay_timer.start() def _add_dimension_to_display(self, occ_dim): ais_dimension = occ_dim.dimension if ais_dimension is not None: self.ais_context.Display(ais_dimension, False) self._displayed_dimensions[ais_dimension] = occ_dim self._redisplay_timer.start() def _remove_dimension_from_display(self, occ_dim): ais_dimension = occ_dim.dimension if ais_dimension is not None: self.ais_context.Remove(ais_dimension, False) self._displayed_dimensions.pop(ais_dimension, None) self._redisplay_timer.start() def _add_item_to_display(self, occ_disp_item): ais_object = occ_disp_item.item if ais_object is not None: self.ais_context.Display(ais_object, False) self._displayed_graphics[ais_object] = occ_disp_item self._redisplay_timer.start() def _remove_item_from_display(self, occ_disp_item): ais_object = occ_disp_item.item if ais_object is not None: self.ais_context.Remove(ais_object, False) self._displayed_graphics.pop(ais_object, None) self._redisplay_timer.start() def on_redisplay_requested(self): self.ais_context.UpdateCurrentViewer() # Recompute bounding box bbox = self.get_bounding_box(self._displayed_shapes.keys()) self.declaration.bbox = BBox(*bbox) # ------------------------------------------------------------------------- # Viewer API # ------------------------------------------------------------------------- def get_bounding_box(self, shapes): """ Compute the bounding box for the given list of shapes. Return values are in 3d coordinate space. Parameters ---------- shapes: List A list of TopoDS_Shape to compute a bbox for Returns ------- bbox: Tuple A tuple of (xmin, ymin, zmin, xmax, ymax, zmax). """ bbox = Bnd_Box() for shape in shapes: BRepBndLib.Add_(shape, bbox) try: pmin = bbox.CornerMin() pmax = bbox.CornerMax() except RuntimeError: return (0, 0, 0, 0, 0, 0) return (pmin.X(), pmin.Y(), pmin.Z(), pmax.X(), pmax.Y(), pmax.Z()) def get_screen_coordinate(self, point): """ Convert a 3d coordinate to a 2d screen coordinate Parameters ---------- (x, y, z): Tuple A 3d coordinate """ return self.v3d_view.Convert(point[0], point[1], point[2], 0, 0) # ------------------------------------------------------------------------- # Rendering parameters # ------------------------------------------------------------------------- def set_chordial_deviation(self, deviation): # Turn up tesselation defaults self.prs3d_drawer.SetMaximalChordialDeviation(deviation) def set_lights(self, lights): viewer = self.v3d_viewer new_lights = [] for d in lights: color, _ = color_to_quantity_color(d.color) if d.type == "directional": if '_' in d.orientation: attr = 'V3d_TypeOfOrientation_{}'.format(d.orientation) else: attr = 'V3d_{}'.format(d.orientation) orientation = getattr(V3d.V3d_TypeOfOrientation, attr, V3d.V3d_Zneg) light = V3d.V3d_DirectionalLight(orientation, color, d.headlight) elif d.type == "spot": light = V3d.V3d_SpotLight(d.position, d.direction, color) light.SetAngle(d.angle) else: light = V3d.V3d_AmbientLight(color) light.SetIntensity(d.intensity) if d.range: light.SetRange(d.range) viewer.AddLight(light) if d.enabled: viewer.SetLightOn(light) new_lights.append(light) for light in self.lights: viewer.DelLight(self.light) self.lights = new_lights def set_draw_boundaries(self, enabled): self.prs3d_drawer.SetFaceBoundaryDraw(enabled) def set_hidden_line_removal(self, enabled): view = self.v3d_view view.SetComputedMode(enabled) self.redraw() def set_antialiasing(self, enabled): self._update_rendering_params() def set_shadows(self, enabled): self._update_rendering_params() def set_reflections(self, enabled): self._update_rendering_params() def set_raytracing(self, enabled): self._update_rendering_params() def set_raytracing_depth(self, depth): self._update_rendering_params() def _update_rendering_params(self, **params): """ Set the rendering parameters of the view Parameters ---------- **params: See Graphic3d_RenderingParams members """ d = self.declaration view = self.v3d_view rendering_params = view.ChangeRenderingParams() if d.raytracing: method = Graphic3d_RM_RAYTRACING else: method = Graphic3d_RM_RASTERIZATION defaults = dict( Method=method, RaytracingDepth=d.raytracing_depth, # IsGlobalIlluminationEnabled=d.raytracing, IsShadowEnabled=d.shadows, IsReflectionEnabled=d.reflections, IsAntialiasingEnabled=d.antialiasing, IsTransparentShadowEnabled=d.shadows, NbMsaaSamples=4, StereoMode=Graphic3d_StereoMode_QuadBuffer, AnaglyphFilter=Graphic3d_RenderingParams. Anaglyph_RedCyan_Optimized, ToReverseStereo=False) defaults.update(**params) for attr, v in defaults.items(): setattr(rendering_params, attr, v) self.redraw() def set_background_gradient(self, gradient): """ Set the background gradient Parameters ---------- gradient: Tuple Gradient parameters Color 1, Color 2, and optionally th fill method """ c1, _ = color_to_quantity_color(gradient[0]) c2, _ = color_to_quantity_color(gradient[1]) fill_method = Aspect_GFM_VER if len(gradient) == 3: attr = 'Aspect_GFM_{}'.format(gradient[2].upper()) fill_method = getattr(Aspect, attr, Aspect_GFM_VER) self.v3d_view.SetBgGradientColors(c1, c2, fill_method, True) def set_shape_color(self, color): self.shape_color = color_to_quantity_color(color) def set_trihedron_mode(self, mode): attr = 'Aspect_TOTP_{}'.format(mode.upper().replace("-", "_")) position = getattr(Aspect, attr) self.v3d_view.TriedronDisplay(position, BLACK, 0.1, V3d.V3d_ZBUFFER) self.redraw() def set_grid_mode(self, mode): if not mode: self.v3d_viewer.DeactivateGrid() else: a, b = mode.title().split("-") grid_type = getattr(Aspect_GridType, f'Aspect_GT_{a}') grid_mode = getattr(Aspect_GridDrawMode, f'Aspect_GDM_{b}') self.v3d_viewer.ActivateGrid(grid_type, grid_mode) def set_grid_colors(self, colors): c1, _ = color_to_quantity_color(colors[0]) c2, _ = color_to_quantity_color(colors[1]) grid = self.v3d_viewer.Grid() grid.SetColors(c1, c2) # ------------------------------------------------------------------------- # Viewer interaction # ------------------------------------------------------------------------- def set_selection_mode(self, mode): """ Set the selection mode. Parameters ---------- mode: String The mode to use (Face, Edge, Vertex, Shell, or Solid) """ ais_context = self.ais_context ais_context.Deactivate() if mode == 'any': for mode in (TopAbs.TopAbs_SHAPE, TopAbs.TopAbs_SHELL, TopAbs.TopAbs_FACE, TopAbs.TopAbs_EDGE, TopAbs.TopAbs_WIRE, TopAbs.TopAbs_VERTEX): ais_context.Activate(AIS_Shape.SelectionMode_(mode)) return attr = 'TopAbs_%s' % mode.upper() mode = getattr(TopAbs, attr, TopAbs.TopAbs_SHAPE) ais_context.Activate(AIS_Shape.SelectionMode_(mode)) def set_display_mode(self, mode): mode = V3D_DISPLAY_MODES.get(mode) if mode is None: return self.ais_context.SetDisplayMode(mode, True) self.redraw() def set_view_mode(self, mode): """ Set the view mode or (or direction) Parameters ---------- mode: String The mode to or direction to view. """ mode = V3D_VIEW_MODES.get(mode.lower()) if mode is None: return self.v3d_view.SetProj(mode) def set_view_projection(self, mode): mode = getattr(Graphic3d_Camera, 'Projection_%s' % mode.title()) self.camera.SetProjectionType(mode) self.redraw() def set_lock_rotation(self, locked): self.widget._lock_rotation = locked def set_lock_zoom(self, locked): self.widget._lock_zoom = locked def zoom_factor(self, factor): self.v3d_view.SetZoom(factor) def rotate_view(self, x=0, y=0, z=0): self.v3d_view.Rotate(x, y, z, True) def turn_view(self, x=0, y=0, z=0): self.v3d_view.Turn(x, y, z, True) def fit_all(self): view = self.v3d_view view.FitAll() view.ZFitAll() self.redraw() def fit_selection(self): if not self._selected_shapes: return # Compute bounding box of the selection view = self.v3d_view pad = 20 bbox = self.get_bounding_box(self._selected_shapes) xmin, ymin = self.get_screen_coordinate(bbox[0:3]) xmax, ymax = self.get_screen_coordinate(bbox[3:6]) cx, cy = int(xmin + (xmax - xmin) / 2), int(ymin + (ymax - ymin) / 2) self.ais_context.MoveTo(cx, cy, view, True) view.WindowFit(xmin - pad, ymin - pad, xmax + pad, ymax + pad) def take_screenshot(self, filename): return self.v3d_view.Dump(filename) # ------------------------------------------------------------------------- # Display Handling # ------------------------------------------------------------------------- def update_selection(self, pos, area, shift): """ Update the selection state """ widget = self.widget view = self.v3d_view ais_context = self.ais_context if area: xmin, ymin, dx, dy = area ais_context.Select(xmin, ymin, xmin + dx, ymin + dy, view, True) elif shift: # multiple select if shift is pressed ais_context.ShiftSelect(True) else: ais_context.Select(True) ais_context.InitSelected() # Lookup the shape declrations based on the selection context selection = {} shapes = [] displayed_shapes = self._displayed_shapes occ_shapes = set(self._displayed_shapes.values()) while ais_context.MoreSelected(): if ais_context.HasSelectedShape(): i = None found = False topods_shape = Topology.cast_shape(ais_context.SelectedShape()) shape_type = topods_shape.ShapeType() attr = str(shape_type).split("_")[-1].lower() + 's' # Try long lookup based on topology for occ_shape in occ_shapes: shape_list = getattr(occ_shape.topology, attr, None) if not shape_list: continue for i, s in enumerate(shape_list): if topods_shape.IsPartner(s): found = True break if found: d = occ_shape.declaration shapes.append(topods_shape) # Insert what was selected into the options info = selection.get(d) if info is None: info = selection[d] = {} selection_info = info.get(attr) if selection_info is None: selection_info = info[attr] = {} selection_info[i] = topods_shape break # Mark it as found we don't know what shape it's from if not found: if None not in selection: selection[None] = {} if attr not in selection[None]: selection[None][attr] = {} info = selection[None][attr] # Just keep incrementing the index info[len(info)] = topods_shape ais_context.NextSelected() if shift: ais_context.UpdateSelected(True) # Set selection self._selected_shapes = shapes self.declaration.selection = ViewerSelection(selection=selection, position=pos, area=area) def update_display(self, change=None): """ Queue an update request """ self._redisplay_timer.start() def clear_display(self): """ Remove all shapes and dimensions drawn """ # Erase all just hides them remove = self.ais_context.Remove for occ_shape in self._displayed_shapes.values(): remove(occ_shape.ais_shape, False) for ais_dim in self._displayed_dimensions.keys(): remove(ais_dim, False) for ais_item in self._displayed_graphics.keys(): remove(ais_item, False) self.gfx_structure.Clear() self.ais_context.UpdateCurrentViewer() def reset_view(self): """ Reset to default zoom and orientation """ self.v3d_view.Reset() @contextmanager def redraw_blocked(self): """ Temporarily stop redraw during """ self._redraw_blocked = True yield self._redraw_blocked = False def redraw(self): if not self._redraw_blocked: self.v3d_view.Redraw() def update(self): """ Redisplay """ self.ais_context.UpdateCurrentViewer()
class TaskDatabase(Atom): """ A database for inter tasks communication. The database has two modes: - an edition mode in which the number of entries and their hierarchy can change. In this mode the database is represented by a nested dict. - a running mode in which the entries are fixed (only their values can change). In this mode the database is represented as a flat list. In running mode the database is thread safe but the object it contains may not be so (dict, list, etc) """ #: Signal used to notify a value changed in the database. #: In edition mode the update is passed as a tuple ('added', path, value) #: for creation, as ('renamed', old, new, value) in case of renaming, #: ('removed', old) in case of deletion or as a list of such tuples. #: In running mode, a 2-tuple (path, value) is sent as entries cannot be #: renamed or removed. notifier = Signal() #: Signal emitted to notify that access exceptions has changed. The update #: is passed as a tuple ('added', path, relative, entry) for creation or as #: ('renamed', path, relative, old, new) in case of renaming of the related #: entry, ('removed', path, relative, old) in case of deletion (if old is #: None all exceptions have been removed) or as a list of such tuples. #: Path indicate the node where the exception is located, relative the #: relative path from the 'path' node to the real location of the entry. access_notifier = Signal() #: Signal emitted to notify that the nodes were modified. The update #: is passed as a tuple ('added', path, name, node) for creation or as #: ('renamed', path, old, new) in case of renaming of the related node, #: ('removed', path, old) in case of deletion or as a list of such tuples. nodes_notifier = Signal() #: List of root entries which should not be listed. excluded = List(default=['threads', 'instrs']) #: Flag indicating whether or not the database entered the running mode. In #: running mode the database is flattened into a list for faster acces. running = Bool(False) def set_value(self, node_path, value_name, value): """Method used to set the value of the entry at the specified path This method can be used both in edition and running mode. Parameters ---------- node_path : unicode Path to the node holding the value to be set value_name : unicode Public key associated with the value to be set, internally converted so that we do not mix value and nodes value : any Actual value to be stored Returns ------- new_val : bool Boolean indicating whether or not a new entry has been created in the database """ new_val = False if self.running: full_path = node_path + '/' + value_name index = self._entry_index_map[full_path] with self._lock: self._flat_database[index] = value self.notifier((node_path + '/' + value_name, value)) else: node = self.go_to_path(node_path) if value_name not in node.data: new_val = True node.data[value_name] = value if new_val: self.notifier(('added', node_path + '/' + value_name, value)) return new_val def get_value(self, assumed_path, value_name): """Method to get a value from the database from its name and a path This method returns the value stored under the specified name. It starts looking at the specified path and if necessary goes up in the hierarchy. Parameters ---------- assumed_path : unicode Path where we start looking for the entry value_name : unicode Name of the value we are looking for Returns ------- value : object Value stored under the entry value_name """ if self.running: index = self._find_index(assumed_path, value_name) return self._flat_database[index] else: node = self.go_to_path(assumed_path) # First check if the entry is in the current node. if value_name in node.data: value = node.data[value_name] return value # Second check if there is a special rule about this entry. elif 'access' in node.meta and value_name in node.meta['access']: path = assumed_path + '/' + node.meta['access'][value_name] return self.get_value(path, value_name) # Finally go one step up in the node hierarchy. else: new_assumed_path = assumed_path.rpartition('/')[0] if assumed_path == new_assumed_path: mes = "Can't find database entry : {}".format(value_name) raise KeyError(mes) return self.get_value(new_assumed_path, value_name) def rename_values(self, node_path, old, new, access_exs=None): """Rename database entries. This method can update the access exceptions attached to them. This method cannot be used in running mode. Parameters ---------- node_path : unicode Path to the node holding the value. old : iterable Old names of the values. new : iterable New names of the values. access_exs : iterable, optional Dict mapping old entries names to how far the access exception is located. """ if self.running: raise RuntimeError('Cannot delete an entry in running mode') node = self.go_to_path(node_path) notif = [] acc_notif = [] access_exs = access_exs if access_exs else {} for i, old_name in enumerate(old): if old_name in node.data: val = node.data.pop(old_name) node.data[new[i]] = val notif.append(('renamed', node_path + '/' + old_name, node_path + '/' + new[i], val)) if old_name in access_exs: count = access_exs[old_name] n = node p = node_path while count: n = n.parent if n.parent else n p, _ = p.rsplit('/', 1) count -= 1 path = n.meta['access'].pop(old_name) n.meta['access'][new[i]] = path acc_notif.append(('renamed', p, path, old_name, new[i])) else: err_str = 'No entry {} in node {}'.format(old_name, node_path) raise KeyError(err_str) # Avoid sending spurious notifications if notif: self.notifier(notif) if acc_notif: self.access_notifier(acc_notif) def delete_value(self, node_path, value_name): """Remove an entry from the specified node This method remove the specified entry from the specified node. It does not handle removing the access exceptions attached to it. This method cannot be used in running mode. Parameters ---------- assumed_path : unicode Path where we start looking for the entry value_name : unicode Name of the value we are looking for """ if self.running: raise RuntimeError('Cannot delete an entry in running mode') else: node = self.go_to_path(node_path) if value_name in node.data: del node.data[value_name] self.notifier(('removed', node_path + '/' + value_name)) else: err_str = 'No entry {} in node {}'.format( value_name, node_path) raise KeyError(err_str) def get_values_by_index(self, indexes, prefix=None): """Access to a list of values using the flat database. Parameters ---------- indexes : list(int) List of index for which values should be returned. prefix : unicode, optional If provided return the values in dict with key of the form : prefix + index. Returns ------- values : list or dict List of requested values in the same order as indexes or dict if prefix was not None. """ if prefix is None: return [self._flat_database[i] for i in indexes] else: return {prefix + str(i): self._flat_database[i] for i in indexes} def get_entries_indexes(self, assumed_path, entries): """ Access to the index in the flattened database for some entries. Parameters ---------- assumed_path : unicode Path to the node in which the values are assumed to be stored. entries : iterable(unicode) Names of the entries for which the indexes should be returned. Returns ------- indexes : dict Dict mapping the entries names to their index in the flattened database. """ return {name: self._find_index(assumed_path, name) for name in entries} def list_accessible_entries(self, node_path): """Method used to get a list of all entries accessible from a node. DO NOT USE THIS METHOD IN RUNNING MODE (ie never in the check method of a task, use a try except clause instead and get_value or get_entries_indexes). Parameters ---------- node_path : unicode Path to the node from which accessible entries should be listed. Returns ------- entries_list : list(unicode) List of entries accessible from the specified node """ entries = [] while True: node = self.go_to_path(node_path) keys = node.data.keys() # Looking for the entries in the node. for key in keys: if not isinstance(node.data[key], DatabaseNode): entries.append(key) # Adding the special access if they are not already in the list. for entry in node.meta.get('access', []): if entry not in entries: entries.append(entry) if node_path != 'root': # Going to the next node. node_path = node_path.rpartition('/')[0] else: break for entry in self.excluded: if entry in entries: entries.remove(entry) return sorted(entries) def list_all_entries(self, path='root', values=False): """List all entries in the database. Parameters ---------- path : unicode, optional Starting node. This parameters is for internal use only. values : bool, optional Whether or not to return the values associated with the entries. Returns ------- paths : list(unicode) or dict if values List of all accessible entries with their full path. """ entries = [] if not values else {} node = self.go_to_path(path) for entry in node.data.keys(): if isinstance(node.data[entry], DatabaseNode): aux = self.list_all_entries(path=path + '/' + entry, values=values) if not values: entries.extend(aux) else: entries.update(aux) else: if not values: entries.append(path + '/' + entry) else: entries[path + '/' + entry] = node.data[entry] if path == 'root': for entry in self.excluded: aux = path + '/' + entry if aux in entries: if not values: entries.remove(aux) else: del entries[aux] return sorted(entries) if not values else entries def add_access_exception(self, node_path, entry_node, entry): """Add an access exception in a node for an entry located in a node below. Parameters ---------- node_path : unicode Path to the node which should hold the exception. entry_node : unicode Absolute path to the node holding the entry. entry : unicode Name of the entry for which to create an exception. """ node = self.go_to_path(node_path) rel_path = entry_node[len(node_path) + 1:] if 'access' in node.meta: access_exceptions = node.meta['access'] access_exceptions[entry] = rel_path else: node.meta['access'] = {entry: rel_path} self.access_notifier(('added', node_path, rel_path, entry)) def remove_access_exception(self, node_path, entry=None): """Remove an access exception from a node for a given entry. Parameters ---------- node_path : unicode Path to the node holding the exception. entry : unicode, optional Name of the entry for which to remove the exception, if not provided all access exceptions will be removed. """ node = self.go_to_path(node_path) if entry: access_exceptions = node.meta['access'] relative_path = access_exceptions[entry] del access_exceptions[entry] else: relative_path = '' del node.meta['access'] self.access_notifier(('removed', node_path, relative_path, entry)) def create_node(self, parent_path, node_name): """Method used to create a new node in the database This method creates a new node in the database at the specified path. This method is not thread safe safe as the hierarchy of the tasks' database is not supposed to change during a measurement but only during the configuration phase Parameters ---------- parent_path : unicode Path to the node parent of the new one node_name : unicode Name of the new node to create """ if self.running: raise RuntimeError('Cannot create a node in running mode') parent_node = self.go_to_path(parent_path) node = DatabaseNode(parent=parent_node) parent_node.data[node_name] = node self.nodes_notifier(('added', parent_path, node_name, node)) def rename_node(self, parent_path, old_name, new_name): """Method used to rename a node in the database Parameters ---------- parent_path : unicode Path to the parent of the node being renamed old_name : unicode Old name of the node. node_name : unicode New name of node """ if self.running: raise RuntimeError('Cannot rename a node in running mode') parent_node = self.go_to_path(parent_path) parent_node.data[new_name] = parent_node.data[old_name] del parent_node.data[old_name] while parent_node: if 'access' not in parent_node.meta: parent_node = parent_node.parent continue access = parent_node.meta['access'].copy() for k, v in access.items(): if old_name in v: new_path = v.replace(old_name, new_name) parent_node.meta['access'][k] = new_path parent_node = parent_node.parent self.nodes_notifier(('renamed', parent_path, old_name, new_name)) def delete_node(self, parent_path, node_name): """Method used to delete an existing node from the database Parameters ---------- parent_path : unicode Path to the node parent of the new one node_name : unicode Name of the new node to create """ if self.running: raise RuntimeError('Cannot delete a node in running mode') parent_node = self.go_to_path(parent_path) if node_name in parent_node.data: del parent_node.data[node_name] else: err_str = 'No node {} at the path {}'.format( node_name, parent_path) raise KeyError(err_str) self.nodes_notifier(('removed', parent_path, node_name)) def copy_node_values(self, node='root'): """Copy the values (ie not subnodes) found in a node. Parameters ---------- node : unicode, optional Path to the node to copy. Returns ------- copy : dict Copy of the node values. """ node = self.go_to_path(node) return { k: v for k, v in node.data.items() if not isinstance(v, DatabaseNode) } def prepare_to_run(self): """Enter a thread safe, flat database state. This is used when tasks are executed. """ self._lock = Lock() self.running = True # Flattening the database by walking all the nodes. index = 0 nodes = [('root', self._database)] mapping = {} datas = [] for (node_path, node) in nodes: for key, val in node.data.items(): path = node_path + '/' + key if isinstance(val, DatabaseNode): nodes.append((path, val)) else: mapping[path] = index index += 1 datas.append(val) # Walking a second time to add the exception to the _entry_index_map, # in reverse order in case an entry has multiple exceptions. for (node_path, node) in nodes[::-1]: access = node.meta.get('access', []) for entry in access: short_path = node_path + '/' + entry full_path = node_path + '/' + access[entry] + '/' + entry mapping[short_path] = mapping[full_path] self._flat_database = datas self._entry_index_map = mapping self._database = None def list_nodes(self): """List all the nodes present in the database. Returns ------- nodes : dict Dictionary storing the nodes by path """ nodes = [('root', self._database)] for (node_path, node) in nodes: for key, val in node.data.items(): if isinstance(val, DatabaseNode): path = node_path + '/' + key nodes.append((path, val)) return dict(nodes) def go_to_path(self, path): """Method used to reach a node specified by a path. """ node = self._database if path == 'root': return node # Decompose the path in database keys keys = path.split('/') # Remove first key (ie 'root' as we are not trying to access it) del keys[0] for key in keys: if key in node.data: node = node.data[key] else: ind = keys.index(key) if ind == 0: err_str = \ 'Path {} is invalid, no node {} in root'.format(path, key) else: err_str = 'Path {} is invalid, no node {} in node\ {}'.format(path, key, keys[ind - 1]) raise KeyError(err_str) return node # ========================================================================= # --- Private API --------------------------------------------------------- # ========================================================================= #: Main container for the database. _database = Typed(DatabaseNode, ()) #: Flat version of the database only used in running mode for perfomances #: issues. _flat_database = List() #: Dict mapping full paths to flat database indexes. _entry_index_map = Dict() #: Lock to make the database thread safe in running mode. _lock = Value() def _find_index(self, assumed_path, entry): """Find the index associated with a path. Only to be used in running mode. """ path = assumed_path while path != 'root': full_path = path + '/' + entry if full_path in self._entry_index_map: return self._entry_index_map[full_path] path = path.rpartition('/')[0] full_path = path + '/' + entry if full_path in self._entry_index_map: return self._entry_index_map[full_path] raise KeyError("Can't find entry matching {}, {}".format( assumed_path, entry))
class AnnealerProcess(Atom): """An annealing process described by a series of steps. """ #: User specified description of the process. description = Str() #: Path under which this process is saved. path = Str() #: Steps describing the annealing process. steps = List(BaseStep, []) #: Current status of the process. status = Enum('Inactive', 'Started', 'Running', 'Completed', 'Stopping', 'Stopped', 'Failed') def save(self, path=None): """Save the process to a json file by serializing the steps. """ path = path or self.path config = dict(steps=[], description=self.description) for s in self.steps: s_config = s.get_preferences_from_members() s_config['type'] = s.__class__.__name__ config['steps'].append(s_config) with open(path, 'w') as f: json.dump(config, f) self.path = path @classmethod def load(cls, path): """Load a process stored in a JSON file. """ with open(path) as f: config = json.load(f) steps = [] for c in config["steps"]: step_cls = STEPS[c.pop('type')] steps.append(step_cls(**c)) return cls(description=config['description'], path=path, steps=steps) def add_step(self, index, step): """Add a step at a given index in the process. If index is None the step is appended instead. """ steps = self.steps[:] if index is None or index >= len(self.steps): steps.append(step) else: steps.insert(index, step) self.steps = steps def move_step(self, old_index, new_index): """Move a step between two positions. """ steps = self.steps[:] step = steps.pop(old_index) steps.insert(new_index, step) # Force a UI notification self.steps = [] self.steps = steps def remove_step(self, index): """Remove a step at a given index. """ steps = self.steps[:] del steps[index] self.steps = steps def start(self, app_state): """Start the process execution. """ queue = Queue() stop_event = Event() crashed_event = Event() #: Reset the plots data app_state.temperature.current_index = 0 app_state.heater_switch.current_index = 0 app_state.heater_regulation.current_index = 0 self._actuator = ActuatorSubprocess(self.path, app_state.get_daq_config(), queue, stop_event, crashed_event) self._monitoring_thread = MonitoringThread(self) self._polling_thread = PollingThread(app_state, queue) self._actuator.start() self.status = 'Started' self._monitoring_thread.start() self._polling_thread.start() app_state.start_plot_timer() def stop(self, force=False): """Stop the process. """ self.status = 'Stopping' if force: self._actuator.terminate() else: self._actuator.stop_event.set() # --- Private API --------------------------------------------------------- #: Subprocess actuator repsonible for the process execution. _actuator = Typed(ActuatorSubprocess) #: Thread responsible for updating the app about the state of progress of #: the process. _polling_thread = Typed(PollingThread) #: Thread updating the state of the process by monitoring teh actuator. _monitoring_thread = Typed(MonitoringThread)
class QtOccViewer(QtControl, ProxyOccViewer): #: Viewer widget widget = Typed(QtViewer3d) #: Update count _update_count = Int(0) #: Displayed Shapes _displayed_shapes = Dict() _ais_shapes = List() #: Shapes shapes = Property(lambda self: self.get_shapes(), cached=True) def _get_display(self): return self.widget._display #: Display display = Property(_get_display, cached=True) def get_shapes(self): return [ c for c in self.children() if not isinstance(c, QtToolkitObject) ] def create_widget(self): self.widget = QtViewer3d(parent=self.parent_widget()) def init_widget(self): super(QtOccViewer, self).init_widget() d = self.declaration widget = self.widget #: Create viewer widget._display = Display(widget.GetHandle()) display = widget._display display.Create() # background gradient self.set_background_gradient(d.background_gradient) self.set_trihedron_mode(d.trihedron_mode) self.set_display_mode(d.display_mode) self.set_selection_mode(d.selection_mode) self.set_view_mode(d.view_mode) self.set_antialiasing(d.antialiasing) self.set_lock_rotation(d.lock_rotation) self.set_lock_zoom(d.lock_zoom) self._update_raytracing_mode() #: Setup callbacks display.register_select_callback(self.on_selection) widget._inited = True # dict mapping keys to functions widget._SetupKeyMap() # display.thisown = False self.init_signals() def init_signals(self): d = self.declaration widget = self.widget for name in widget._callbacks.keys(): if hasattr(d, name): cb = getattr(d, name) widget._callbacks[name].append(cb) def init_layout(self): super(QtOccViewer, self).init_layout() for child in self.children(): self.child_added(child) display = self.display display.OnResize() def child_added(self, child): if not isinstance(child, QtToolkitObject): self.get_member('shapes').reset(self) child.observe('shape', self.update_display) self.update_display() else: super(QtOccViewer, self).child_added(child) def child_removed(self, child): if not isinstance(child, QtToolkitObject): self.get_member('shapes').reset(self) child.unobserve('shape', self.update_display) else: super(QtOccViewer, self).child_removed(child) # ------------------------------------------------------------------------- # Viewer API # ------------------------------------------------------------------------- def get_bounding_box(self, shapes): """ Compute the bounding box for the given list of shapes. Return values are in 3d coordinate space. Parameters ---------- shapes: List A list of TopoDS_Shape to compute a bbox for Returns ------- bbox: Tuple A tuple of (xmin, ymin, zmin, xmax, ymax, zmax). """ bbox = Bnd_Box() for shape in shapes: brepbndlib_Add(shape, bbox) return bbox.Get() def get_screen_coordinate(self, point): """ Convert a 3d coordinate to a 2d screen coordinate Parameters ---------- (x, y, z): Tuple A 3d coordinate """ return self.display.View.Convert(*point) def set_antialiasing(self, enabled): if enabled: self.display.EnableAntiAliasing() else: self.display.DisableAntiAliasing() def set_shadows(self, enabled): self._update_raytracing_mode() def set_reflections(self, enabled): self._update_raytracing_mode() def _update_raytracing_mode(self): d = self.declaration display = self.display if not hasattr(display.View, 'SetRaytracingMode'): return if d.shadows or d.reflections: display.View.SetRaytracingMode() if d.shadows: display.View.EnableRaytracedShadows() if d.reflections: display.View.EnableRaytracedReflections() if d.antialiasing: display.View.EnableRaytracedAntialiasing() else: display.View.DisableRaytracingMode() def set_background_gradient(self, gradient): self.display.set_bg_gradient_color(*gradient) def set_trihedron_mode(self, mode): attr = 'Aspect_TOTP_{}'.format(mode.upper().replace("-", "_")) position = getattr(Aspect, attr) self.display.View.TriedronDisplay(position, Quantity_NOC_BLACK, 0.1, V3d.V3d_ZBUFFER) if self.widget._inited: self.display.Context.UpdateCurrentViewer() def set_selection_mode(self, mode): """ Call SetSelectionMode<mode> on the display. """ attr = 'SetSelectionMode{}'.format(mode.title()) handler = getattr(self.display, attr, None) if handler is not None: handler() def set_display_mode(self, mode): if mode == 'shaded': self.display.SetModeShaded() elif mode == 'hlr': self.display.SetModeHLR() elif mode == 'wireframe': self.display.SetModeWireFrame() def set_view_mode(self, mode): """ Call View_<mode> on the display and refit as needed. """ attr = 'View_{}'.format(mode.title()) handler = getattr(self.display, attr, None) if handler is not None: handler() self.display.FitAll() def set_lock_rotation(self, locked): self.widget._lock_rotation = locked def set_lock_zoom(self, locked): self.widget._lock_zoom = locked def zoom_factor(self, factor): self.display.ZoomFactor(factor) def fit_all(self): self.display.FitAll() def fit_selection(self): if not self.display.selected_shapes: return # Compute bounding box of the selection bbox = self.get_bounding_box(self.display.selected_shapes) xmin, ymin = self.get_screen_coordinate(bbox[0:3]) xmax, ymax = self.get_screen_coordinate(bbox[3:6]) cx, cy = int(xmin + (xmax - xmin) / 2), int(ymin + (ymax - ymin) / 2) self.display.MoveTo(cx, cy) pad = 20 self.display.ZoomArea(xmin - pad, ymin - pad, xmax + pad, ymax + pad) def take_screenshot(self, filename): return self.display.View.Dump(filename) # ------------------------------------------------------------------------- # Display Handling # ------------------------------------------------------------------------- def on_selection(self, selection, *args, **kwargs): d = self.declaration selection = [] for shape in self.display.selected_shapes: if shape in self._displayed_shapes: selection.append(self._displayed_shapes[shape].declaration) else: log.warn("shape {} not in {}".format(shape, self._displayed_shapes)) #d.selection = selection d.selection( ViewerSelectionEvent(selection=selection, parameters=args, options=kwargs)) # def _queue_update(self,change): # self._update_count +=1 # timed_call(0,self._check_update,change) # # def _dequeue_update(self,change): # # Only update when all changes are done # self._update_count -=1 # if self._update_count !=0: # return # self.update_shape(change) def update_display(self, change=None): """ Queue an update request """ self._update_count += 1 timed_call(10, self._do_update) def clear_display(self): display = self.display # Erase all just hides them display.Context.PurgeDisplay() display.Context.RemoveAll() def _expand_shapes(self, shapes): expansion = [] for s in shapes: for c in s.children(): if isinstance(c, OccPart): expansion.extend(self._expand_shapes(c.shapes)) if hasattr(s, 'shapes'): expansion.extend(self._expand_shapes(s.shapes)) else: expansion.append(s) return expansion def _do_update(self): # Only update when all changes are done self._update_count -= 1 if self._update_count != 0: return #: TO try: display = self.display self.clear_display() displayed_shapes = {} ais_shapes = [] #log.debug("_do_update {}") #: Expand all parts otherwise we lose the material information shapes = self._expand_shapes(self.shapes[:]) for shape in shapes: d = shape.declaration if not shape.shape: log.error("{} has no shape property!".format(shape)) continue try: if isinstance(shape.shape, BRepBuilderAPI_MakeShape): s = shape.shape.Shape() else: s = shape.shape except: log.error("{} failed to create shape: {}".format( shape, traceback.format_exc())) continue displayed_shapes[s] = shape #: If a material is given material = getattr( Graphic3d, 'Graphic3d_NOM_{}'.format( d.material.upper())) if d.material else None #: If last shape update = shape == shapes[-1] ais_shapes.append( display.DisplayShape(s, color=d.color, material=material, transparency=d.transparency, update=update, fit=not self._displayed_shapes)) self._ais_shapes = ais_shapes self._displayed_shapes = displayed_shapes # Update bounding box # TODO: Is there an API for this? bbox = self.get_bounding_box(displayed_shapes.keys()) self.declaration.bbox = BBox(*bbox) except: log.error("Failed to display shapes: {}".format( traceback.format_exc()))
class MuxerModel(Atom): """Class that defines the Model for the data muxer Attributes ---------- data_muxer : dataportal.muxer.api.DataMuxer The data_muxer holds the non-time-aligned data. Upon asking the data_muxer to reformat its data into time-aligned bins, a dataframe is returned run_header: metadatastore.api.Document The bucket of information from the data broker that contains all non-data information column_models : atom.dict.Dict Dictionary that is analogous to the col_info property of the dataportal.muxer.data.DataMuxer object scalar_columns : atom.list.List The list of columns names whose cells contain 0-D arrays (single values) line_columns : atom.list.List The list of column names whose cells contain 1-D arrays image_columns : atom.list.List The list of column names whose cells contain 2-D arrays volume_columns : atom.list.List The list of column names whos cells contain 3-D arrays scalar_columns_visible : atom.scalars.Bool Instructs the GUI to show/hide the scalar info line_columns_visible : atom.scalars.Bool Instructs the GUI to show/hide the line info image_columns_visible : atom.scalars.Bool Instructs the GUI to show/hide the image info volume_columns_visible : atom.scalars.Bool Instructs the GUI to show/hide the volume info info : atom.scalars.Str A short string describing the `data_muxer` attribute of the Atom MuxerModel new_data_callbacks : atom.list.List List of callbacks that care when the data_muxer gets new data. Callback functions should expect no information to be passed. auto_updating : atom.Bool Is the databroker going to be regularly asked for data? True -> yes. False -> no update_rate : atom.Int The rate at which the databroker will be asked for new data """ column_models = Dict() scalar_columns = List(item=ColumnModel) line_columns = List(item=ColumnModel) image_columns = List(item=ColumnModel) volume_columns = List(item=ColumnModel) scalar_columns_visible = Bool(False) line_columns_visible = Bool(False) image_columns_visible = Bool(False) volume_columns_visible = Bool(False) data_muxer = Typed(DataMuxer) header = Typed(Document) info = Str() new_data_callbacks = List() auto_updating = Bool(False) update_rate = Int(1000) # in ms def __init__(self): # initialize everything to be the equivalent of None. It would seem # that the first accessing of an Atom instance attribute causes the # creation of that attribute, thus triggering the @observe decorator. with self.suppress_notifications(): self.column_models = {} self.scalar_columns = [] self.line_columns = [] self.image_columns = [] self.volume_columns = [] self.data_muxer = None self.header = None self.info = 'No run header received yet' self.new_data_callbacks = [] @observe('header') def run_header_changed(self, changed): print('Run header has been changed, creating a new data_muxer') self.info = 'Run {}'.format(self.header.scan_id) with self.suppress_notifications(): self.data_muxer = None self.get_new_data() def new_run_header(self, changed): """Observer function for the `header` attribute of the SearchModels """ self.header = changed['value'] def get_new_data(self): """Hit the dataportal to first see if there is new data and, if so, grab it """ print('getting new data from the data broker') events = get_events(self.header) if self.data_muxer is None: # this will automatically trigger the key updating self.data_muxer = DataMuxer.from_events(events) else: self.data_muxer.append_events(events) for data_cb in self.new_data_callbacks: data_cb() # update the column information self._verify_column_info() for data_cb in self.new_data_callbacks: data_cb() @observe('data_muxer') def new_muxer(self, changed): # data_muxer object has been changed. Remake the columns print('new data muxer received') self._verify_column_info() def _verify_column_info(self): print('verifying column information') updated_cols = [] for col_name, col_model in self.column_models.items(): muxer_col_info = self.data_muxer.col_info.get(col_name, None) if muxer_col_info: # if the values are the same, no magic updates happen, otherwise # the UI gets magically updated col_model.dim = muxer_col_info.ndim col_model.name = muxer_col_info.name col_model.upsample = muxer_col_info.upsample col_model.downsample = muxer_col_info.downsample col_model.shape = muxer_col_info.shape col_model.data_muxer = self.data_muxer updated_cols.append(col_name) else: # remove the column model self.column_models.pop(col_name) for col_name, col_info in self.data_muxer.col_info.items(): if col_name in updated_cols: # column has already been accounted for, move on to the next one continue # insert a new column model print(col_info) self.column_models[col_name] = ColumnModel( data_muxer=self.data_muxer, dim=col_info.ndim, name=col_name, shape=col_info.shape) self._update_column_sortings() def _update_column_sortings(self): print('updating column sortings') mapping = {0: set(), 1: set(), 2: set(), 3: set()} for col_name, col_model in self.column_models.items(): mapping[col_model.dim].add(col_model) # update the column key lists, if necessary self.scalar_columns = [] self.line_columns = [] self.image_columns = [] self.volume_columns = [] self.scalar_columns = list(mapping[0]) self.line_columns = list(mapping[1]) self.image_columns = list(mapping[2]) self.volume_columns = list(mapping[3]) # set the GUI elements to be visible/hidden if there are/aren't any # column_models self.scalar_columns_visible = len(self.scalar_columns) != 0 self.line_columns_visible = len(self.line_columns) != 0 self.image_columns_visible = len(self.image_columns) != 0 self.volume_columns_visible = len(self.volume_columns) != 0
class ScalarCollection(Atom): """ ScalarCollection is a bundle of ScalarModels. The ScalarCollection has an instance of a DataMuxer which notifies it of new data which then updates its ScalarModels. When instantiated, the data_muxer instance is asked for the names of its columns. All columns which represent scalar values are then shoved into ScalarModels and the ScalarCollection manages the ScalarModels. Attributes ---------- data_muxer : replay.pipeline.pipeline.DataMuxer The data manager backing the ScalarModel. The DataMuxer's new_data signal is connected to the notify_new_data function of the ScalarModel so that the ScalarModel can decide what to do when the DataMuxer receives new data. scalar_models : atom.Dict The collection of scalar_models that the ScalarCollection knows about data_cols : atom.List The names of the data sets that are in the DataMuxer redraw_every : atom.Float The frequency with which to redraw the plot. The meaning of this parameter changes based on `redraw_type` redraw_type : {'max rate', 's'} Gives meaning to the float stored in `redraw_every`. Should be read as 'Update the plot at a rate of `redraw_every` per `redraw_type`'. Since there are only the two options in `ScalarCollection`, it should be understood that the previous statement is only relevant when 's' is selected as the `redraw_type`. If `max_rate` is selected, then the plot will attempt to update itself as fast as data is coming in. Beware that this may cause significant performance issues if your data rate is > 20 Hz update_rate : atom.Str Formatted rate that new data is coming in. x : atom.Str The name of the x-axis that the `scalar_models` should be plotted against """ # dictionary of lines that can be toggled on and off scalar_models = Dict(key=Str(), value=ScalarModel) # the thing that holds all the data data_muxer = Typed( DataMuxer ) # the Document that holds all non-data associated with the data_muxer header = Typed(Document) # The scan id of this data set scan_id = Int() # name of the x axis x = Str() # index of x in data_cols # x_index = data_cols.index(x) x_index = Int() # name of the column to align against bin_on = Str() x_is_time = Bool(False) # name of all columns that the data muxer knows about data_cols = List() derived_cols = List() # should the pandas dataframe plotting use subplots for each column single_plot = Bool(False) # shape of the subplots ncols = Range(low=0) nrows = Range(low=0) # 0 here # ESTIMATING # the current set of data to perform peak estimates for estimate_target = Str() # the result of the estimates, stored as a dictionary # The list of peak parameters to plot estimate_stats = Typed(OrderedDict) estimate_plot = List() # the index of the data set to perform estimates for # estimate_index = data_cols.index(estimate_target) estimate_index = Int() # NORMALIZING normalize_target = Str() # should the data be normalized? normalize = Bool(False) # MPL PLOTTING STUFF _fig = Typed(Figure) _ax = Typed(Axes) # configuration properties for the 1-D plot _conf = Typed(ScalarConfig) # CONTROL OF THE PLOT UPDATE SPEED redraw_every = Float(default=1) redraw_type = Enum('max rate', 's') update_rate = Str() # the last time that the plot was updated _last_update_time = Typed(datetime) # the last frame that the plot was updated _last_update_frame = Int() # the number of times that `notify_new_data` has been called since the last # update _num_updates = Int() def __init__(self): with self.suppress_notifications(): super(ScalarCollection, self).__init__() # plotting initialization self._fig = Figure(figsize=(1, 1)) self._fig.set_tight_layout(True) self._ax = self._fig.add_subplot(111) self._conf = ScalarConfig(self._ax) self.redraw_type = 's' self.estimate_plot = ['cen', 'x_at_max'] self.estimate_stats = OrderedDict() def init_scalar_models(self): self.scalar_models.clear() line_artist, = self._ax.plot([], [], label=nodata_str) self.scalar_models[nodata_str] = ScalarModel(line_artist=line_artist, name=nodata_str, is_plotting=True) def new_data_muxer(self, changed): """Function to be registered with a MugglerModel on its `muxer` attribute Parameters ---------- changed : dict Changed is emitted by Atom and has the following keys: {'value', 'object', 'type', 'name', 'oldvalue'} """ print('new_data_muxer callback function triggered in ' 'ScalarCollection') self.data_muxer = changed['value'] @observe('data_muxer') def update_datamuxer(self, changed): print('data muxer update triggered') with self.suppress_notifications(): if self.data_muxer is None: self.data_cols = [nodata_str] self.x = nodata_str self.estimate_target = self.x self.estimate_index = self.data_cols.index(self.x) self.normalize_target = self.x self.bin_on = self.x self.init_scalar_models() return # get the column names with dimensionality equal to zero # print('data muxer col info by ndim: {}'.format(self.data_muxer.col_info_by_ndim)) data_cols = [ col_info.name for col_info in self.data_muxer.col_info_by_ndim[0] ] derived_cols = [] # init the x axis to be the first column name x = data_cols[0] # default to time x_is_time = True estimate_target = x estimate_index = data_cols.index(estimate_target) # don't bin by any of the columns by default and plot by time bin_on = x # blow away scalar models self.scalar_models.clear() self._ax.cla() for name in data_cols: # create a new line artist and scalar model line_artist, = self._ax.plot([], [], label=name, marker='D') self.scalar_models[name] = ScalarModel(line_artist=line_artist, name=name, is_plotting=True) # add the estimate name = 'peak stats' line_artist, = self._ax.plot([], [], 'ro', label=name, markersize=15) self.scalar_models[name] = ScalarModel(line_artist=line_artist, name=name, is_plotting=True) for model in self.scalar_models.values(): model.observe('is_plotting', self.reformat_view) derived_cols.append(name) # trigger the updates # print('data_cols', data_cols) # for model_name, model in six.iteritems(self.scalar_models): # print(model.state) self.derived_cols = [] self.derived_cols = derived_cols self.data_cols = [] self.data_cols = data_cols self.x_is_time = x_is_time self._last_update_time = datetime.utcnow() self.bin_on = bin_on self.x = x self.estimate_target = x self.estimate_index = estimate_index self.normalize_target = x self.set_state() self.get_new_data_and_plot() @observe('data_cols') def update_col_names(self, changed): print('data_cols changed: {}'.format(self.data_cols)) @observe('x_is_time') def update_x_axis(self, changed): lbl = 'Time (s)' if not changed['value']: lbl = self.x self._conf.xlabel = lbl self.get_new_data_and_plot() @observe('x') def update_x(self, changed): print('x updated: {}'.format(self.x)) self._conf.xlabel = self.x self.get_new_data_and_plot() self.x_index = self.data_cols.index(self.x) @observe('alignment_col') def update_alignment_col(self, changed): # check with the muxer for the columns that can be plotted against sliceable = self.data_muxer.align_against(self.bin_on) for name, scalar_model in six.iteritems(self.scalar_models): if name == 'fit' or name == 'peak stats': continue if not sliceable[name]: # turn off the plotting and disable the check box scalar_model.is_plotting = False scalar_model.can_plot = False else: # enable the check box but don't turn on the plotting scalar_model.can_plot = True self.get_new_data_and_plot() @observe('estimate_plot') def update_estimate(self, changed): self.reformat_view() @observe('estimate_target') def update_estimate_target(self, changed): self.estimate_index = self.data_cols.index(self.estimate_target) @observe('normalize') def update_normalize(self, changed): self.get_new_data_and_plot() def set_state(self): plotx = getattr(self.header, 'plotx', None) ploty = getattr(self.header, 'ploty', None) if plotx: self.x = plotx print('plotx: {}'.format(plotx)) print('ploty: {}'.format(ploty)) for name, model in self.scalar_models.items(): print(name, model, 'name in ploty: ', name in ploty) self.scalar_models[name].is_plotting = name in ploty if ploty: self.x_is_time = False def header_changed(self, changed): """Callback that should be connected to whatever Atom instance has a reference to a header. This callback will parse the header for any interesting bits of information that modifies the state of replay Parameters ---------- changed : dict The dict that gets emitted when the header attribute of an Atom class is changed Notes ----- Known Keys plotx : str The column that should be on the x-axis ploty : list The columns that should be shown on the y-axis """ value = changed['value'] print('header: {}'.format(value)) print('vars(header): {}'.format(vars(value))) self.header = value def print_state(self): """Print the, uh, state """ for model_name, model in six.iteritems(self.scalar_models): print(model.get_state()) def notify_new_column(self, new_columns): """Function to call when there is a new column in the data muxer Parameters ---------- new_columns: list The new column name that the data muxer knows about """ scalar_cols = self.data_muxer.keys(dim=0) alignable = self.data_muxer.align_against(self.bin_on, self.data_cols) for name, is_plottable in six.iteritems(alignable): if name in new_columns and not self.data_muxer.col_dims[name]: line_artist, = self._ax.plot([], [], label=name) self.scalar_models[name] = ScalarModel(line_artist=line_artist, name=name) self.scalar_models[name].can_plot = is_plottable def format_number(self, number): return '{:.5f}'.format(number) def estimate(self): """Return a dictionary of the vital stats of a 'peak'""" stats = OrderedDict() print('self.fit_target: {}'.format(self.fit_target)) print('self.alignment_col: {}'.format(self.bin_on)) print('self.x: {}'.format(self.x)) other_cols = [self.x, self.estimate_target] if self.normalize: other_cols.append(self.normalize_target) print('other_cols: {}'.format(other_cols)) time, data = self.data_muxer.get_values(ref_col=self.bin_on, other_cols=other_cols) x = np.asarray(data[self.x]) y = np.asarray(data[self.estimate_target]) if self.normalize: y = y / np.asarray(data[self.normalize_target]) # print('x, len(x): {}, {}'.format(x, len(x))) # print('y, len(y): {}, {}'.format(y, len(y))) fn = lambda num: '{:.5}'.format(num) # Center of peak stats['ymin'] = y.min() stats['ymax'] = y.max() stats['avg_y'] = fn(np.average(y)) stats['x_at_ymin'] = x[y.argmin()] stats['x_at_ymax'] = x[y.argmax()] # Calculate CEN from derivative zero_cross = np.where( np.diff(np.sign(y - (stats['ymax'] + stats['ymin']) / 2)))[0] if zero_cross.size == 2: stats['cen'] = (fn(x[zero_cross].sum() / 2), fn((stats['ymax'] + stats['ymin']) / 2)) elif zero_cross.size == 1: stats['cen'] = x[zero_cross[0]] if zero_cross.size == 2: fwhm = x[zero_cross] stats['width'] = fwhm[1] - fwhm[0] stats['fwhm_left'] = (fn(fwhm[0]), fn(y[zero_cross[0]])) stats['fwhm_right'] = (fn(fwhm[1]), fn(y[zero_cross[1]])) # Center of mass stats['center_of_mass'] = fn((x * y).sum() / y.sum()) # # # extra_models = [] # for model_name, model in six.iteritems(self.scalar_models): # if model_name in self.estimate_stats: # line_artist = self.scalar_models[model_name].line_artist # self._ax.lines.remove(line_artist) # line_artist.remove() # del line_artist # print(line_artist) # model = self.scalar_models.pop(model_name) # model = None # # # create new line artists # for name, val in six.iteritems(stats): # line_artist, = self._ax.axvline(label=name, color = 'r', # linewidth=2, # x=val) # self.scalar_models[name] = ScalarModel(line_artist=line_artist, # name=name, # can_plot=True, # is_plotting=False) # self.data_cols = [] # self.data_cols = list(six.iterkeys(self.scalar_models)) # trigger the automatic update of the GUI self.estimate_stats = OrderedDict() self.estimate_stats = stats def notify_new_data(self): """ Function to call when there is new data in the data muxer Parameters ---------- new_data : list List of names of updated columns from the data muxer """ # self._num_updates += 1 # redraw = False # if self.redraw_type == 's': # if ((datetime.utcnow() - self._last_update_time).total_seconds() # >= self.redraw_every): # redraw = True # else: # # do nothing # pass # elif self.redraw_type == 'max rate': # redraw = True # if self.bin_on in new_data: # # update all the data in the line plot # y_names = list(self.scalar_models) # else: # # find out which new_data keys overlap with the data that is # # supposed to be shown on the plot # y_names = [] # for model_name, model in six.iteritems(self.scalar_models): # if model.is_plotting and model.name in new_data: # y_names.append(model.name) # if redraw: self.get_new_data_and_plot() # self.estimate() def get_new_data_and_plot(self, y_names=None): """ Get the data from the data muxer for column `data_name` sampled at the time_stamps of `VariableModel.x` Parameters ---------- data_name : list, optional List of the names of columns in the data muxer. If None, get all data from the data muxer """ if self.data_muxer is None: return if self.x_is_time: self.plot_by_time() else: self.plot_by_x() def plot_by_time(self): df = self.data_muxer._dataframe data_dict = { data_name: { 'x': df[data_name].index.tolist(), 'y': df[data_name].tolist() } for data_name in df.columns if data_name in self.data_cols } self._plot(data_dict) def plot_by_x(self): interpolation = {name: 'linear' for name in self.data_cols} agg = {name: np.mean for name in self.data_cols} df = self.data_muxer.bin_on(self.x, interpolation=interpolation, agg=agg, col_names=self.data_cols) x_axis = df[self.x].val.values data_dict = { data_name[0]: { 'x': x_axis, 'y': df[data_name].tolist() } for data_name in df } self._plot(data_dict) def _plot(self, data_dict): for dname, dvals in data_dict.items(): if dname in self.data_cols: self.scalar_models[dname].set_data(dvals['x'], dvals['y']) # self.scalar_models[dname].is_plotting = True self.reformat_view() def plot_by_x_old(self, y_names): # interpolation = {name: 'linear' for name in self.data_muxer.col_info.keys()} # agg = {name: np.mean for name in self.data_muxer.col_info.keys()} df = self.data_muxer.bin_on( self.x) #, interpolation=interpolation, agg=agg) self._fig.clf() print('fig before df.plot call', self._fig) self._ax = self._fig.add_subplot(111) df.plot(subplots=False, ax=self._ax) print('fig after df.plot call', self._fig) return time, data = self.data_muxer.get_values(ref_col=self.bin_on, other_cols=other_cols) ref_data = data.pop(self.x) if self.normalize: norm_data = data.pop(self.normalize_target) # switch between x axis as data and x axis as time if self.x_is_data: ref_data_vals = ref_data.values else: ref_data_vals = time # print('ref_data_vals: {}'.format(ref_data_vals)) if self.scalar_models[self.x].is_plotting: self.scalar_models[self.x].set_data(x=ref_data_vals, y=ref_data) for dname, dvals in six.iteritems(data): # print('{}: {}'.format(dname, id(self.scalar_models[dname]))) if self.normalize: dvals = dvals / norm_data self.scalar_models[dname].set_data(x=ref_data_vals, y=dvals) # manage the fitting if self.fit_target is not '': target_data = ref_data if self.fit_target != self.x: target_data = data[self.fit_target] self.multi_fit_controller.set_xy(x=ref_data_vals, y=target_data.values) if self.multi_fit_controller.guess: self.multi_fit_controller.do_guess() if self.multi_fit_controller.autofit: self.multi_fit_controller.fit() try: self.scalar_models['fit'].set_data( x=ref_data_vals, y=self.multi_fit_controller.best_fit) except RuntimeError: # thrown when x and y are not the same length pass self.reformat_view() self.update_rate = "{0:.2f} s<sup>-1</sup>".format( float(self._num_updates) / (datetime.utcnow() - self._last_update_time).total_seconds()) self._num_updates = 0 self._last_update_time = datetime.utcnow() # except KeyError: # pass # self._ax.axvline(x=self.estimate_stats['cen'], linewidth=2, color='r') # self._ax.axvline(x=self.estimate_stats['x_at_max'], linewidth=4, # color='k') def reformat_view(self, *args, **kwargs): """ Recompute the limits, rescale the view, reformat the legend and redraw the canvas """ # ignore the args and kwargs. They are here so that any function can be # connected to this one x_data = [] y_data = [] try: y_val = self.estimate_stats['avg_y'] except KeyError: y_val = 1 for plot in self.estimate_plot: try: stats = self.estimate_stats[plot] except KeyError: continue try: stats_len = len(stats) except TypeError: stats_len = 1 if stats_len == 2: x_data.append(stats[0]) y_data.append(stats[1]) else: x_data.append(stats) y_data.append(y_val) try: self.scalar_models['peak stats'].set_data(x=x_data, y=y_data) except KeyError: # data muxer hasn't been created yet pass try: legend_pairs = [(v.line_artist, k) for k, v in six.iteritems(self.scalar_models) if v.line_artist.get_visible()] if legend_pairs: arts, labs = zip(*legend_pairs) self._ax.legend(arts, labs).draggable() else: self._ax.legend(legend_pairs) self._ax.relim(visible_only=True) self._ax.autoscale_view(tight=True) self._ax.grid(self._conf.grid) self._ax.set_ylabel(self._conf.ylabel) self._ax.set_xlabel(self._conf.xlabel) self._ax.set_title(self._conf.title) self._fig.canvas.draw() # self._ax.figure.canvas.draw() # print('current figure id: {}'.format(id(self._fig))) except AttributeError as ae: # should only happen once pass
class JDF_Assign(Atom): """describes a jdf assign statement. defaults to a single pattern P(1) at position 1,1""" assign_type=List(default=['P(1)']).tag(desc="list of assigns") pos_assign=List(default=[(1,1),]).tag(desc="list of tuples of position") shot_assign=Unicode().tag(desc="shot mod table name") comment=Unicode().tag(desc="comment on assign") short_name=Unicode().tag(desc="short name used for display in html table") def dup_assign(self): """duplicates assign as separate object using string parsing functionality""" dup_str, dup_comment=parse_comment(self.jdf_output) return JDF_Assign(tempstr=dup_str, comment=dup_comment) @property def A_nums(self): """all A numbers in assign_type""" return find_A_nums(self.assign_str) @property def P_nums(self): """all P numbers in assign_type""" return find_P_nums(self.assign_str) def _default_pos_assign(self): return [(1,1)] def _default_short_name(self): return self.comment.split(" ")[0] def xy_offset(self, x_start, x_step, y_start, y_step): return [(x_start+(p[0]-1)*x_step, y_start+(p[1]-1)*y_step) for p in self.pos_assign] @property def assign_str(self): """utility combination of assign_type list as string""" return "+".join(self.assign_type) @property def jdf_output(self): """produces output string for jdf""" pos_asgn="" for pos in self.pos_assign: pos_asgn+="({x},{y}),".format(x=pos[0], y=pos[1]) pos_asgn=pos_asgn[:-1] shot_asgn=format_comment(self.shot_assign, sep=",", fmt_str="{0} {1}") asgn_comment=format_comment(self.comment) return "\tASSIGN {asgn_type} -> ({pos_asgn}{shot_asgn}) {asgn_comment}".format( asgn_type=self.assign_str, pos_asgn=pos_asgn, shot_asgn=shot_asgn, asgn_comment=asgn_comment) def __init__(self, **kwargs): """Processes kwargs to allow string definition to be passes as well""" tempstr=kwargs.pop("tempstr", "ASSIGN P(1) -> ((1,1))") kwargs["comment"]=kwargs.get("comment", "") assign_type=kwargs.get("assign_type", tempstr.split("ASSIGN")[1].split("->")[0].strip().split("+")) kwargs["assign_type"]=[unicode(at) for at in assign_type] kwargs["pos_assign"]=kwargs.get("pos_assign", []) kwargs["shot_assign"]=kwargs.get("shot_assign", "") x_num=kwargs.pop("x_num", 1) y_num=kwargs.pop("y_num", 1) if kwargs["pos_assign"]==[]: for item in tempstr.split("->")[1].partition("(")[2].rpartition(")")[0].split(")"): if "(" in item: xcor, ycor=item.split("(")[1].split(",") if "-" in xcor: xcor_start, xcor_end=xcor.split("-") xcor_list=[int(xcor_start)+num for num in range(int(xcor_end))] elif "*" in xcor: xcor_list= [int(1)+num for num in range(int(x_num))] else: xcor_list=[int(xcor)] if "-" in ycor: ycor_start, ycor_end=ycor.split("-") ycor_list=[int(ycor_start)+num for num in range(int(ycor_end))] elif "*" in ycor: ycor_list= [int(1)+num for num in range(int(y_num))] else: ycor_list=[int(ycor)] kwargs["pos_assign"].extend([(x,y) for y in ycor_list for x in xcor_list]) #kwargs["pos_assign"].append((int(xcor), int(ycor))) elif "," in item: kwargs["shot_assign"]=unicode(item.split(",")[1].strip()) super(JDF_Assign, self).__init__(**kwargs)
class QtContainer(QtConstraintsWidget, ProxyContainer): """ A Qt implementation of an Enaml ProxyContainer. """ #: A reference to the toolkit widget created by the proxy. widget = Typed(QContainer) #: A list of the contents constraints for the widget. contents_cns = List() #: Whether or not this container owns its layout. A container which #: does not own its layout is not responsible for laying out its #: children on a resize event, and will proxy the call to its owner. _owns_layout = Bool(True) #: The object which has taken ownership of the layout for this #: container, if any. _layout_owner = Value() #: The LayoutManager instance to use for solving the layout system #: for this container. _layout_manager = Value() #: The function to use for refreshing the layout on a resize event. _refresh = Callable(lambda *args, **kwargs: None) #: The table of offsets to use during a layout pass. _offset_table = List() #: The table of (index, updater) pairs to use during a layout pass. _layout_table = List() def _default_contents_cns(self): """ Create the contents constraints for the container. The contents contraints are generated by combining the user padding with the margins returned by 'contents_margins' method. Returns ------- result : list The list of casuarius constraints for the content. """ d = self.declaration margins = self.contents_margins() top, right, bottom, left = map(sum, zip(d.padding, margins)) cns = [ d.contents_top == (d.top + top), d.contents_left == (d.left + left), d.contents_right == (d.left + d.width - right), d.contents_bottom == (d.top + d.height - bottom), ] return cns #-------------------------------------------------------------------------- # Initialization API #-------------------------------------------------------------------------- def create_widget(self): """ Creates the QContainer widget. """ self.widget = QContainer(self.parent_widget()) def init_widget(self): """ Initialize the widget. """ super(QtContainer, self).init_widget() self.widget.resized.connect(self.on_resized) def init_layout(self): """ Initialize the layout of the widget. """ super(QtContainer, self).init_layout() self.init_cns_layout() def init_cns_layout(self): """ Initialize the constraints layout. """ # Layout ownership can only be transferred *after* this init # layout method is called, since layout occurs bottom up. So, # we only initialize a layout manager if ownership is unlikely # to be transferred. if not self.will_transfer(): offset_table, layout_table = self._build_layout_table() cns = self._generate_constraints(layout_table) manager = LayoutManager() manager.initialize(cns) self._offset_table = offset_table self._layout_table = layout_table self._layout_manager = manager self._refresh = self._build_refresher(manager) self._update_sizes() #-------------------------------------------------------------------------- # Signal Handlers #-------------------------------------------------------------------------- def on_resized(self): """ Update the position of the widgets in the layout. This makes a layout pass over the descendents if this widget owns the responsibility for their layout. """ # The _refresh function is generated on every relayout and has # already taken into account whether or not the container owns # the layout. self._refresh() #-------------------------------------------------------------------------- # Public Layout Handling #-------------------------------------------------------------------------- def relayout(self): """ Rebuild the constraints layout for the widget. If this object does not own the layout, the call is proxied to the layout owner. """ if self._owns_layout: item = self.widget_item old_hint = item.sizeHint() self.init_cns_layout() self._refresh() new_hint = item.sizeHint() # If the size hint constraints are empty, it indicates that # they were previously cleared. In this case, the layout # system must be notified to rebuild its constraints, even # if the numeric size hint hasn't changed. if old_hint != new_hint or not self.size_hint_cns: self.size_hint_updated() else: self._layout_owner.relayout() def replace_constraints(self, old_cns, new_cns): """ Replace constraints in the given layout. This method can be used to selectively add/remove/replace constraints in the layout system, when it is more efficient than performing a full relayout. Parameters ---------- old_cns : list The list of casuarius constraints to remove from the the current layout system. new_cns : list The list of casuarius constraints to add to the current layout system. """ if self._owns_layout: manager = self._layout_manager if manager is not None: with size_hint_guard(self): manager.replace_constraints(old_cns, new_cns) self._update_sizes() self._refresh() else: self._layout_owner.replace_constraints(old_cns, new_cns) def clear_constraints(self, cns): """ Clear the given constraints from the current layout. Parameters ---------- cns : list The list of casuarius constraints to remove from the current layout system. """ if self._owns_layout: manager = self._layout_manager if manager is not None: manager.replace_constraints(cns, []) else: self._layout_owner.clear_constraints(cns) def contents_margins(self): """ Get the contents margins for the container. The contents margins are added to the user provided padding to determine the final offset from a layout box boundary to the corresponding content line. The default content margins are zero. This method can be reimplemented by subclasses to supply different margins. Returns ------- result : tuple A tuple of 'top', 'right', 'bottom', 'left' contents margins to use for computing the contents constraints. """ return (0, 0, 0, 0) def contents_margins_updated(self): """ Notify the system that the contents margins have changed. """ old_cns = self.contents_cns del self.contents_cns new_cns = self.contents_cns self.replace_constraints(old_cns, new_cns) #-------------------------------------------------------------------------- # Private Layout Handling #-------------------------------------------------------------------------- def _layout(self): """ The layout callback invoked by the layout manager. This iterates over the layout table and calls the geometry updater functions. """ # We explicitly don't use enumerate() to generate the running # index because this method is on the code path of the resize # event and hence called *often*. The entire code path for a # resize event is micro optimized and justified with profiling. offset_table = self._offset_table layout_table = self._layout_table running_index = 1 for offset_index, updater in layout_table: dx, dy = offset_table[offset_index] new_offset = updater(dx, dy) offset_table[running_index] = new_offset running_index += 1 def _update_sizes(self): """ Update the min/max/best sizes for the underlying widget. This method is called automatically at the proper times. It should not normally need to be called by user code. """ widget = self.widget widget.setSizeHint(self.compute_best_size()) widget.setMinimumSize(self.compute_min_size()) widget.setMaximumSize(self.compute_max_size()) def _build_refresher(self, manager): """ Build the refresh function for the container. Parameters ---------- manager : LayoutManager The layout manager to use when refreshing the layout. """ # The return function is a hyper optimized (for Python) closure # in order minimize the amount of work which is performed on the # code path of the resize event. This is explicitly not idiomatic # Python code. It exists purely for the sake of efficiency, # justified with profiling. mgr_layout = manager.layout d = self.declaration layout = self._layout width_var = d.width height_var = d.height widget = self.widget width = widget.width height = widget.height return lambda: mgr_layout(layout, width_var, height_var, (width(), height())) def _build_layout_table(self): """ Build the layout table for this container. A layout table is a pair of flat lists which hold the required objects for laying out the child widgets of this container. The flat table is built in advance (and rebuilt if and when the tree structure changes) so that it's not necessary to perform an expensive tree traversal to layout the children on every resize event. Returns ------- result : (list, list) The offset table and layout table to use during a resize event. """ # The offset table is a list of (dx, dy) tuples which are the # x, y offsets of children expressed in the coordinates of the # layout owner container. This owner container may be different # from the parent of the widget, and so the delta offset must # be subtracted from the computed geometry values during layout. # The offset table is updated during a layout pass in breadth # first order. # # The layout table is a flat list of (idx, updater) tuples. The # idx is an index into the offset table where the given child # can find the offset to use for its layout. The updater is a # callable provided by the widget which accepts the dx, dy # offset and will update the layout geometry of the widget. zero_offset = (0, 0) offset_table = [zero_offset] layout_table = [] queue = deque((0, child) for child in self.children()) # Micro-optimization: pre-fetch bound methods and store globals # as locals. This method is not on the code path of a resize # event, but it is on the code path of a relayout. If there # are many children, the queue could potentially grow large. push_offset = offset_table.append push_item = layout_table.append push = queue.append pop = queue.popleft QtConstraintsWidget_ = QtConstraintsWidget QtContainer_ = QtContainer isinst = isinstance # The queue yields the items in the tree in breadth-first order # starting with the immediate children of this container. If a # given child is a container that will share its layout, then # the children of that container are added to the queue to be # added to the layout table. running_index = 0 while queue: offset_index, item = pop() if isinst(item, QtConstraintsWidget_): push_item((offset_index, item.geometry_updater())) push_offset(zero_offset) running_index += 1 if isinst(item, QtContainer_): if item.transfer_layout_ownership(self): for child in item.children(): push((running_index, child)) return offset_table, layout_table def _generate_constraints(self, layout_table): """ Creates the list of casuarius LinearConstraint objects for the widgets for which this container owns the layout. This method walks over the items in the given layout table and aggregates their constraints into a single list of casuarius LinearConstraint objects which can be given to the layout manager. Parameters ---------- layout_table : list The layout table created by a call to _build_layout_table. Returns ------- result : list The list of casuarius LinearConstraints instances to pass to the layout manager. """ # The list of raw casuarius constraints which will be returned # from this method to be added to the casuarius solver. cns = self.contents_cns[:] cns.extend(self.declaration._hard_constraints()) cns.extend(self.declaration._collect_constraints()) # The first element in a layout table item is its offset index # which is not relevant to constraints generation. for _, updater in layout_table: child = updater.item d = child.declaration cns.extend(d._hard_constraints()) if isinstance(child, QtContainer): if child.transfer_layout_ownership(self): cns.extend(d._collect_constraints()) cns.extend(child.contents_cns) else: cns.extend(child.size_hint_cns) else: cns.extend(d._collect_constraints()) cns.extend(child.size_hint_cns) return cns #-------------------------------------------------------------------------- # Auxiliary Methods #-------------------------------------------------------------------------- def transfer_layout_ownership(self, owner): """ A method which can be called by other components in the hierarchy to gain ownership responsibility for the layout of the children of this container. By default, the transfer is allowed and is the mechanism which allows constraints to cross widget boundaries. Subclasses should reimplement this method if different behavior is desired. Parameters ---------- owner : Declarative The component which has taken ownership responsibility for laying out the children of this component. All relayout and refresh requests will be forwarded to this component. Returns ------- results : bool True if the transfer was allowed, False otherwise. """ if not self.declaration.share_layout: return False self._owns_layout = False self._layout_owner = owner del self._layout_manager del self._refresh del self._offset_table del self._layout_table return True def will_transfer(self): """ Whether or not the container expects to transfer its layout ownership to its parent. This method is predictive in nature and exists so that layout managers are not senslessly created during the bottom-up layout initialization pass. It is declared public so that subclasses can override the behavior if necessary. """ d = self.declaration return d.share_layout and isinstance(self.parent(), QtContainer) def compute_min_size(self): """ Calculates the minimum size of the container which would allow all constraints to be satisfied. If the container's resist properties have a strength less than 'medium', the returned size will be zero. If the container does not own its layout, the returned size will be invalid. Returns ------- result : QSize A (potentially invalid) QSize which is the minimum size required to satisfy all constraints. """ d = self.declaration shrink = ('ignore', 'weak') if d.resist_width in shrink and d.resist_height in shrink: return QSize(0, 0) if self._owns_layout and self._layout_manager is not None: w, h = self._layout_manager.get_min_size(d.width, d.height) if d.resist_width in shrink: w = 0 if d.resist_height in shrink: h = 0 return QSize(w, h) return QSize() def compute_best_size(self): """ Calculates the best size of the container. The best size of the container is obtained by computing the min size of the layout using a strength which is much weaker than a normal resize. This takes into account the size of any widgets which have their resist clip property set to 'weak' while still allowing the window to be resized smaller by the user. If the container does not own its layout, the returned size will be invalid. Returns ------- result : QSize A (potentially invalid) QSize which is the best size that will satisfy all constraints. """ if self._owns_layout and self._layout_manager is not None: d = self.declaration w, h = self._layout_manager.get_min_size(d.width, d.height, weak) return QSize(w, h) return QSize() def compute_max_size(self): """ Calculates the maximum size of the container which would allow all constraints to be satisfied. If the container's hug properties have a strength less than 'medium', or if the container does not own its layout, the returned size will be the Qt maximum. Returns ------- result : QSize A (potentially invalid) QSize which is the maximum size allowable while still satisfying all constraints. """ d = self.declaration expanding = ('ignore', 'weak') if d.hug_width in expanding and d.hug_height in expanding: return QSize(16777215, 16777215) if self._owns_layout and self._layout_manager is not None: w, h = self._layout_manager.get_max_size(d.width, d.height) if w < 0 or d.hug_width in expanding: w = 16777215 if h < 0 or d.hug_height in expanding: h = 16777215 return QSize(w, h) return QSize(16777215, 16777215)
class QDT(IDT, Sat_Qubit): base_name = "QDT" @private_property def view_window(self): return QDTView(agent=self) fq0 = SProperty().tag(desc="center frequency of oscillator") @fq0.getter def _get_fq0(self, f, f0, ft_mult, eta, epsinf, Ct_mult, Dvv, Np, W): ls = self._get_Lamb_shift(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, W=W, Dvv=Dvv, Np=Np, Ct_mult=Ct_mult) return sqrt(f * (f - 2.0 * ls)) @fq0.setter def _get_Ej_get_fq0(self, fq0, Ec): return (h * fq0) ^ 2 / (8.0 * Ec) fFWHM = SProperty().tag( desc="center frequency of oscillator plus half width") @fFWHM.getter def _get_fFWHM(self, f, f0, ft_mult, eta, epsinf, Ct_mult, Dvv, Np, W, dephasing, dephasing_slope): ls = self._get_Lamb_shift(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, W=W, Dvv=Dvv, Np=Np, Ct_mult=Ct_mult) gamma = self._get_coupling(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, W=W, Dvv=Dvv, Np=Np, Ct_mult=Ct_mult) fplus = sqrt(f * (f - 2.0 * ls + 2.0 * gamma)) fminus = sqrt(f * (f - 2.0 * ls - 2.0 * gamma)) return fplus, fminus, fplus - fminus + dephasing + dephasing_slope * f fluxfq0 = SProperty().tag(desc="center frequency of oscillator as voltage") @fluxfq0.getter def _get_fluxfq0(self, f, f0, ft_mult, eta, epsinf, Ct_mult, Dvv, Np, W, Ct, Ejmax): fq0 = self._get_fq0(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, Ct_mult=Ct_mult, Dvv=Dvv, Np=Np, W=W) return self._get_flux_from_fq(fq=fq0, Ct=Ct, Ejmax=Ejmax) fluxfFWHM = SProperty().tag(desc="FWHM of oscillator") @fluxfFWHM.getter def _get_fluxfFWHM(self, f, f0, ft_mult, eta, epsinf, Ct_mult, Dvv, Np, W, Ct, Ejmax): fplus, fminus, fwhm = self._get_fFWHM(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, Ct_mult=Ct_mult, Dvv=Dvv, Np=Np, W=W) Vminus = self._get_flux_from_fq(fq=fplus, Ct=Ct, Ejmax=Ejmax) Vplus = self._get_flux_from_fq(fq=fminus, Ct=Ct, Ejmax=Ejmax) return Vplus, Vminus, Vplus - Vminus Vfq0 = SProperty().tag(desc="center frequency of oscillator as voltage") @Vfq0.getter def _get_Vfq0(self, f, f0, ft_mult, eta, epsinf, Ct_mult, Dvv, Np, W, Ct, Ejmax, offset, flux_factor): fq0 = self._get_fq0(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, Ct_mult=Ct_mult, Dvv=Dvv, Np=Np, W=W) return self._get_voltage_from_flux_par(fq=fq0, Ct=Ct, Ejmax=Ejmax, offset=offset, flux_factor=flux_factor) VfFWHM = SProperty().tag(desc="FWHM of oscillator") @VfFWHM.getter def _get_VfFWHM(self, f, f0, ft_mult, eta, epsinf, Ct_mult, Dvv, Np, W, Ct, Ejmax, offset, flux_factor): fplus, fminus, fwhm = self._get_fFWHM(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, Ct_mult=Ct_mult, Dvv=Dvv, Np=Np, W=W) Vminus = self._get_voltage_from_flux_par(fq=fplus, Ct=Ct, Ejmax=Ejmax, offset=offset, flux_factor=flux_factor) Vplus = self._get_voltage_from_flux_par(fq=fminus, Ct=Ct, Ejmax=Ejmax, offset=offset, flux_factor=flux_factor) return Vplus, Vminus, Vplus - Vminus Vfq0_many = SProperty().tag(sub=True) @Vfq0_many.getter def _get_Vfq0_many(self, f, f0, ft_mult, eta, epsinf, Ct_mult, Dvv, Np, W, Ct, Ejmax, offset, flux_factor): fq0 = self._get_fq0(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, Ct_mult=Ct_mult, Dvv=Dvv, Np=Np, W=W) return self._get_voltage_from_flux_par_many(fq=fq0, Ct=Ct, Ejmax=Ejmax, offset=offset, flux_factor=flux_factor) # GL=Float(1.0) # simple_S_qdt=SProperty().tag(sub=True) # @simple_S_qdt.getter # def _get_simple_S_qdt(self, f, f0, ft_mult, eta, epsinf, Ct_mult, K2, Np, Ct, L, dL, vf, L_IDT, GL): # Ga=self._get_Ga(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, Ct_mult=Ct_mult, K2=K2, Np=Np) # Ba=self._get_Ba(f=f, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, Ct_mult=Ct_mult, K2=K2, Np=Np) # w=2*pi*f # YL=-1.0j/(w*L) # # k=2*pi*f/vf # jkL=1.0j*k*L_IDT # P33plusYL=Ga+1.0j*Ba+1.0j*w*Ct-1.0j/w*dL+YL # S11=S22=-Ga/P33plusYL*exp(-jkL) # S12=S21=exp(-jkL)+S11 # S13=S23=S32=S31=1.0j*sqrt(2.0*Ga*GL)/P33plusYL*exp(-jkL/2.0) # S33=(YL-Ga+1.0j*Ba+1.0j*w*Ct-1.0j/w*dL)/P33plusYL # return (S11, S12, S13, # S21, S22, S23, # S31, S32, S33) @private_property def fixed_fq(self): return linspace(self.fixed_fq_min, self.fixed_fq_max, self.N_fixed_fq).astype(float64) N_fixed_fq = Int(500) fixed_fq_max = Float() fixed_fq_min = Float(0.01) def _default_fixed_freq_max(self): return 2.0 * self.f0 def _default_fixed_fq_max(self): return 2.0 * self.f0 def _default_fixed_fq_min(self): return 0.0001 #*self.f0 def _default_N_fixed(self): return 400 calc_p_guess = Bool(False) fitter = Typed(LorentzianFitter, ()) #fit.gamma=0.05 flux_indices = List() def _default_flux_indices(self): return [range(len(self.fixed_fq))] @private_property def flat_flux_indices(self): return [n for ind in self.flux_indices for n in ind] fit_indices = List() #.tag(private=True) def _default_fit_indices(self): return [range(len(self.fixed_freq))] @private_property def flat_indices(self): return [n for ind in self.fit_indices for n in ind] @private_property def MagAbs(self): #(S11, S12, S13, # S21, S22, S23, # S31, S32, S33)=self.fixed_S magind = { "S11": 0, "S12": 1, "S13": 2, "S21": 3, "S22": 4, "S23": 5, "S31": 6, "S32": 7, "S33": 8 }[self.magabs_type] magcom = self.fixed_S[:, magind, :] #magcom={"S11" : S11, "S12" : S12, "S13" : S13, # "S21" : S21, "S22" : S22, "S23" : S23, # "S31" : S31, "S32" : S32, "S33" : S33}[self.magabs_type] return absolute(magcom) #if self.bgsub_type=="dB": # return 10.0**(self.MagdB/10.0) #magabs=absolute(self.Magcom) #if self.bgsub_type=="Abs": # return self.bgsub(magabs) #return magabs @private_property def fit_params(self): MagAbsSq = (self.MagAbs**2).transpose() if self.fitter.fit_params is None: self.fitter.full_fit(x=self.fixed_fq[self.flat_flux_indices] / 1e9, y=MagAbsSq, indices=self.flat_indices, gamma=self.fitter.gamma) if self.calc_p_guess: self.fitter.make_p_guess( self.fixed_fq[self.flat_flux_indices] / 1e9, y=MagAbsSq, indices=self.flat_indices, gamma=self.fitter.gamma) return self.fitter.fit_params @private_property def MagAbsFit(self): return sqrt( self.fitter.reconstruct_fit( self.fixed_fq[self.flat_flux_indices] / 1e9, self.fit_params)).transpose() # YL=SProperty() # @YL.getter # def _get_YL(self, w, L): # if self.YL_type=="constant": # return self.YL # elif self.YL_type=="inductor": # return -1.0j/(w*L) @private_property def fixed_S(self): w = 2 * pi * self.fixed_freq L_arr = self._get_L(fq=self.fixed_fq) if self.S_type == "simple": return array([ self._get_simple_S(f=self.fixed_freq, YL=-1.0j / (w * L)) for L in L_arr ]) P = self.fixed_P[0] #return self.PtoS(*P, YL=self.YL) return array([self.PtoS(*P, YL=-1.0j / (w * L)) for L in L_arr]) lamb_shifted_transmon_energy = SProperty() @lamb_shifted_transmon_energy.getter def _get_lamb_shifted_transmon_energy(self, Ej, Ec, m, f0, ft_mult, eta, epsinf, W, Dvv, Np, Ct_mult): Em = -Ej + sqrt(8.0 * Ej * Ec) * (m + 0.5) - (Ec / 12.0) * ( 6.0 * m**2 + 6.0 * m + 3.0) if m == 0: return Em Emm1 = -Ej + sqrt( 8.0 * Ej * Ec) * (m - 1 + 0.5) - (Ec / 12.0) * (6.0 * (m - 1)**2 + 6.0 * (m - 1) + 3.0) fq = (Em - Emm1) / h fls = self._get_Lamb_shift(f=fq, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, W=W, Dvv=Dvv, Np=Np, Ct_mult=Ct_mult) return Em + h * fls lamb_shifted_transmon_energy_levels = SProperty() @lamb_shifted_transmon_energy_levels.getter def _get_lamb_shifted_transmon_energy_levels(self, Ej, f0, ft_mult, eta, epsinf, W, Dvv, Np, Ct_mult, Ec, n_energy): return [ self._get_lamb_shifted_transmon_energy(Ej=Ej, Ec=Ec, m=m, f0=f0, ft_mult=ft_mult, eta=eta, epsinf=epsinf, W=W, Dvv=Dvv, Np=Np, Ct_mult=Ct_mult) for m in range(n_energy) ]
class BridgedApplication(Application): """ An abstract implementation of an Enaml application. This serves as a base class for both Android and iOS applications and provides support for the python event loop, the development server and the bridge. """ __id__ = Int(-1) #: Keep screen on by setting the WindowManager flag keep_screen_on = Bool() #: Statusbar color statusbar_color = Unicode() #: Application lifecycle state must be set by the implementation state = Enum('created', 'paused', 'resumed', 'stopped', 'destroyed') #: Width of the screen in dp width = Float(strict=False) #: Height of the screen in dp height = Float(strict=False) #: Screen orientation orientation = Enum('portrait', 'landscape', 'square') #: View to display within the activity view = Value() #: Factory to create and show the view. It takes the app as the first arg load_view = Callable() #: If true, debug bridge statements debug = Bool() #: Use dev server dev = Unicode() _dev_session = Value() #: Event loop loop = Instance(EventLoop) #: Events to send to the bridge _bridge_queue = List() #: Time last sent _bridge_max_delay = Float(0.005) #: Time last sent _bridge_last_scheduled = Float() #: Entry points to load plugins plugins = Dict() # ------------------------------------------------------------------------- # Defaults # ------------------------------------------------------------------------- def _default_loop(self): """ Get the event loop based on what libraries are available. """ return EventLoop.default() def _default_plugins(self): """ Get entry points to load any plugins installed. The build process should create an "entry_points.json" file with all of the data from the installed entry points. """ plugins = {} try: with open('entry_points.json') as f: entry_points = json.load(f) for ep, obj in entry_points.items(): plugins[ep] = [] for name, src in obj.items(): plugins[ep].append(Plugin(name=name, source=src)) except Exception as e: print("Failed to load entry points {}".format(e)) return plugins # ------------------------------------------------------------------------- # BridgedApplication Constructor # ------------------------------------------------------------------------- def __init__(self, *args, **kwargs): """ Initialize the event loop error handler. Subclasses must properly initialize the proxy resolver. """ super(BridgedApplication, self).__init__(*args, **kwargs) if self.dev: self.start_dev_session() self.init_error_handler() self.load_plugin_widgets() self.load_plugin_factories() # ------------------------------------------------------------------------- # Abstract API Implementation # ------------------------------------------------------------------------- def start(self): """ Start the application's main event loop using either twisted or tornado. """ #: Schedule a load view if given and remote debugging is not active #: the remote debugging init call this after dev connection is ready if self.load_view and self.dev != "remote": self.deferred_call(self.load_view, self) self.loop.start() def stop(self): """ Stop the application's main event loop. """ self.loop.stop() def deferred_call(self, callback, *args, **kwargs): """ Invoke a callable on the next cycle of the main event loop thread. Parameters ---------- callback : callable The callable object to execute at some point in the future. *args, **kwargs Any additional positional and keyword arguments to pass to the callback. """ return self.loop.deferred_call(callback, *args, **kwargs) def timed_call(self, ms, callback, *args, **kwargs): """ Invoke a callable on the main event loop thread at a specified time in the future. Parameters ---------- ms : int The time to delay, in milliseconds, before executing the callable. callback : callable The callable object to execute at some point in the future. *args, **kwargs Any additional positional and keyword arguments to pass to the callback. """ return self.loop.timed_call(ms, callback, *args, **kwargs) def is_main_thread(self): """ Indicates whether the caller is on the main gui thread. Returns ------- result : bool True if called from the main gui thread. False otherwise. """ return False # ------------------------------------------------------------------------- # App API Implementation # ------------------------------------------------------------------------- def has_permission(self, permission): """ Return a future that resolves with the result of the permission """ raise NotImplementedError def request_permissions(self, permissions): """ Return a future that resolves with the result of the permission request """ raise NotImplementedError # ------------------------------------------------------------------------- # EventLoop API Implementation # ------------------------------------------------------------------------- def init_error_handler(self): """ When an error occurs, set the error view in the App """ self.loop.set_error_handler(self.handle_error) def create_future(self): """ Create a future object using the EventLoop implementation """ return self.loop.create_future() def run_iteration(self): """ Run an iteration of the event loop """ return self.loop.run_iteration() def add_done_callback(self, future, callback): """ Add a callback on a future object put here so it can be implemented with different event loops. Parameters ----------- future: Future or Deferred Future implementation for the current EventLoop callback: callable Callback to invoke when the future is done """ if future is None: raise bridge.BridgeReferenceError( "Tried to add a callback to a nonexistent Future. " "Make sure you pass the `returns` argument to your JavaMethod") return self.loop.add_done_callback(future, callback) def set_future_result(self, future, result): """ Set the result of the future Parameters ----------- future: Future or Deferred Future implementation for the current EventLoop result: object Result to set """ return self.loop.set_future_result(future, result) # ------------------------------------------------------------------------- # Bridge API Implementation # ------------------------------------------------------------------------- def show_view(self): """ Show the current `app.view`. This will fade out the previous with the new view. """ raise NotImplementedError def get_view(self): """ Get the root view to display. Make sure it is properly initialized. """ view = self.view if not view.is_initialized: view.initialize() if not view.proxy_is_active: view.activate_proxy() return view.proxy.widget def show_error(self, msg): """ Show the error view with the given message on the UI. """ self.send_event(bridge.Command.ERROR, msg) def send_event(self, name, *args, **kwargs): """ Send an event to the native handler. This call is queued and batched. Parameters ---------- name : str The event name to be processed by MainActivity.processMessages. *args: args The arguments required by the event. **kwargs: kwargs Options for sending. These are: now: boolean Send the event now """ n = len(self._bridge_queue) # Add to queue self._bridge_queue.append((name, args)) if n == 0: # First event, send at next available time self._bridge_last_scheduled = time() self.deferred_call(self._bridge_send) return elif kwargs.get('now'): self._bridge_send(now=True) return # If it's been over 5 ms since we last scheduled, run now dt = time() - self._bridge_last_scheduled if dt > self._bridge_max_delay: self._bridge_send(now=True) def force_update(self): """ Force an update now. """ #: So we don't get out of order self._bridge_send(now=True) def _bridge_send(self, now=False): """ Send the events over the bridge to be processed by the native handler. Parameters ---------- now: boolean Send all pending events now instead of waiting for deferred calls to finish. Use this when you want to update the screen """ if len(self._bridge_queue): if self.debug: print("======== Py --> Native ======") for event in self._bridge_queue: print(event) print("===========================") self.dispatch_events(bridge.dumps(self._bridge_queue)) self._bridge_queue = [] def dispatch_events(self, data): """ Send events to the bridge using the system specific implementation. """ raise NotImplementedError def process_events(self, data): """ The native implementation must use this call to """ events = bridge.loads(data) if self.debug: print("======== Py <-- Native ======") for event in events: print(event) print("===========================") for event in events: if event[0] == 'event': self.handle_event(event) def handle_event(self, event): """ When we get an 'event' type from the bridge handle it by invoking the handler and if needed sending back the result. """ result_id, ptr, method, args = event[1] obj = None result = None try: obj, handler = bridge.get_handler(ptr, method) result = handler(*[v for t, v in args]) except bridge.BridgeReferenceError as e: #: Log the event, don't blow up here msg = "Error processing event: {} - {}".format(event, e).encode("utf-8") print(msg) #self.show_error(msg) except: #: Log the event, blow up in user's face msg = "Error processing event: {} - {}".format( event, traceback.format_exc()).encode("utf-8") print(msg) self.show_error(msg) raise finally: if result_id: if hasattr(obj, '__nativeclass__'): sig = getattr(type(obj), method).__returns__ else: sig = type(result).__name__ self.send_event( bridge.Command.RESULT, #: method result_id, bridge.msgpack_encoder(sig, result) #: args ) def handle_error(self, callback): """ Called when an error occurs in an event loop callback. By default, sets the error view. """ self.loop.log_error(callback) msg = "\n".join( ["Exception in callback %r" % callback, traceback.format_exc()]) self.show_error(msg.encode('utf-8')) # ------------------------------------------------------------------------- # AppEventListener API Implementation # ------------------------------------------------------------------------- def on_events(self, data): """ Called when the bridge sends an event. For instance the return result of a method call or a callback from a widget event. """ #: Pass to event loop thread self.deferred_call(self.process_events, data) def on_pause(self): """ Called when the app is paused. """ pass def on_resume(self): """ Called when the app is resumed. """ pass def on_stop(self): """ Called when the app is stopped. """ #: Called from thread, make sure the correct thread detaches pass def on_destroy(self): """ Called when the app is destroyed. """ self.deferred_call(self.stop) # ------------------------------------------------------------------------- # Dev Session Implementation # ------------------------------------------------------------------------- def start_dev_session(self): """ Start a client that attempts to connect to the dev server running on the host `app.dev` """ try: from .dev import DevServerSession session = DevServerSession.initialize(host=self.dev) session.start() #: Save a reference self._dev_session = session except: self.show_error(traceback.format_exc()) # ------------------------------------------------------------------------- # Plugin implementation # ------------------------------------------------------------------------- def get_plugins(self, group): """ Was going to use entry points but that requires a ton of stuff which will be extremely slow. """ return self.plugins.get(group, []) def load_plugin_widgets(self): """ Pull widgets added via plugins using the `enaml_native_widgets` entry point. The entry point function must return a dictionary of Widget declarations to add to the core api. def install(): from charts.widgets.chart_view import BarChart, LineChart return { 'BarChart': BarChart, 'LineCart': LineChart, } """ from enamlnative.widgets import api for plugin in self.get_plugins(group='enaml_native_widgets'): get_widgets = plugin.load() for name, widget in iter(get_widgets()): #: Update the core api with these widgets setattr(api, name, widget) def load_plugin_factories(self): """ Pull widgets added via plugins using the `enaml_native_ios_factories` or `enaml_native_android_factories` entry points. The entry point function must return a dictionary of Widget declarations to add to the factories for this platform. def install(): return { 'MyWidget':my_widget_factory, # etc... } """ raise NotImplementedError
class LogPlugin(Plugin): """Plugin managing the application logging. """ #: List of installed handlers. handler_ids = List(Unicode()) #: List of installed filters. filter_ids = List(Unicode()) #: Model which can be used to display the log in the GUI. It is associated #: to a handler attached to the root logger. gui_model = Typed(LogModel) def add_handler(self, id, handler=None, logger='', mode=None): """Add a handler to the specified logger. Parameters ---------- id : unicode Id of the new handler. This id should be unique. handler : logging.Handler, optional Handler to add. logger : unicode, optional Name of the logger to which the handler should be added. By default the handler is added to the root logger. mode : {'ui', }, optional Conveninence to add a simple logger. If this argument is specified, handler will be ignored and the command will return useful references (the model to which can be connected a ui for the 'ui' mode). Returns ------- refs : list List of useful reference, empty if no mode is selected. """ refs = [] if not handler: if mode and mode == 'ui': model = LogModel() handler = GuiHandler(model=model) refs.append(model) else: logger = logging.getLogger(__name__) msg = ('Missing handler or recognised mode when adding ' 'log handler under id %s to logger %s') logger.info(msg, id, logger) return [] name = logger logger = logging.getLogger(name) logger.addHandler(handler) self._handlers[id] = (handler, name) self.handler_ids = list(self._handlers.keys()) if refs: return refs def remove_handler(self, id): """Remove the specified handler. Parameters ---------- id : unicode Id of the handler to remove. """ handlers = self._handlers if id in handlers: handler, logger_name = handlers.pop(id) logger = logging.getLogger(logger_name) logger.removeHandler(handler) for filter_id in self.filter_ids: infos = self._filters[filter_id] if infos[1] == id: del self._filters[filter_id] self.filter_ids = list(self._filters.keys()) self.handler_ids = list(self._handlers.keys()) def add_filter(self, id, filter, handler_id): """Add a filter to the specified handler. Parameters ---------- id : unicode Id of the filter to add. filter : object Filter to add to the specified handler (object implemeting a filter method). handler_id : unicode Id of the handler to which this filter should be added """ if not hasattr(filter, 'filter'): logger = logging.getLogger(__name__) logger.warn('Filter does not implemet a filter method') return handlers = self._handlers if handler_id in handlers: handler, _ = handlers[handler_id] handler.addFilter(filter) self._filters[id] = (filter, handler_id) self.filter_ids = list(self._filters.keys()) else: logger = logging.getLogger(__name__) logger.warn('Handler {} does not exist') def remove_filter(self, id): """Remove the specified filter. Parameters ---------- id : unicode Id of the filter to remove. """ filters = self._filters if id in filters: filter, handler_id = filters.pop(id) handler, _ = self._handlers[handler_id] handler.removeFilter(filter) self.filter_ids = list(self._filters.keys()) def set_formatter(self, handler_id, formatter): """Set the formatter of the specified handler. Parameters ---------- handler_id : unicode Id of the handler whose formatter shoudl be set. formatter : Formatter Formatter for the handler. """ handlers = self._handlers handler_id = str(handler_id) if handler_id in handlers: handler, _ = handlers[handler_id] handler.setFormatter(formatter) else: logger = logging.getLogger(__name__) logger.warn('Handler {} does not exist') # ---- Private API -------------------------------------------------------- # Mapping between handler ids and handler, logger name pairs. _handlers = Dict(Unicode(), Tuple()) # Mapping between filter_id and filter, handler_id pairs. _filters = Dict(Unicode(), Tuple())
class FirstPassTemplateCompiler(block.FirstPassBlockCompiler): """ The first pass template compiler. This compiler generates the code which builds the compiler nodes for the template definition. The main entry point is the 'compile' class method. """ #: The const names collected during traversal. const_names = List() @classmethod def compile(cls, node, args, local_names, filename): """ Invoke the compiler for the given node. The generated code object expects the SCOPE_KEY to be passed as one of the arguments. The code object will return a 2-tuple of compiler node list and const expression value tuple. Parameters ---------- node : Template The enaml ast Template node of interest. args : list The list of argument names which will be passed to the code object when it is invoked. local_names : set The set of local names which are available to the code object. This should be the combination of const expression names and template parameter names. filename : str The filename of the node being compiled. Returns ------- result : tuple A 2-tuple of (code, index_map) which is the generated code object for the first compiler pass, and a mapping of ast node to relevant compiler node index. """ compiler = cls() compiler.filename = filename compiler.local_names = local_names cg = compiler.code_generator # Setup the block for execution. cmn.fetch_helpers(cg) cmn.make_node_list(cg, cmn.count_nodes(node)) # Dispatch the visitors. compiler.visit(node) # Setup the parameters and generate the code object. cg.name = node.name cg.firstlineno = node.lineno cg.newlocals = True cg.args = args code = cg.to_code() # Union the two index maps for use by the second compiler pass. final_index_map = dict(compiler.index_map) final_index_map.update(compiler.aux_index_map) return (code, final_index_map) def visit_Template(self, node): # No pragmas are supported yet for template nodes. cmn.warn_pragmas(node, self.filename) # Claim the index for the compiler node index = len(self.index_map) self.index_map[node] = index # Setup the line number for the template. cg = self.code_generator cg.set_lineno(node.lineno) # Create the template compiler node and store in the node list. cmn.load_helper(cg, 'template_node') cg.load_fast(cmn.SCOPE_KEY) cg.call_function(1) cmn.store_node(cg, index) # Visit the body of the template. for item in node.body: self.visit(item) # Update the internal node ids for the hierarchy. cmn.load_node(cg, 0) cg.load_attr('update_id_nodes') cg.call_function() cg.pop_top() # Load the compiler node list for returning. cg.load_fast(cmn.NODE_LIST) # Load the const names for returning. for name in self.const_names: cg.load_fast(name) cg.build_tuple(len(self.const_names)) # Create and return the return value tuple. cg.build_tuple(2) cg.return_value() def visit_ConstExpr(self, node): # Keep track of the const name for loading for return. self.const_names.append(node.name) # Setup the line number for the const expr. cg = self.code_generator cg.set_lineno(node.lineno) # Generate the code for the expression. names = self.local_names cmn.safe_eval_ast(cg, node.expr.ast, node.name, node.lineno, names) # Validate the type of the expression value. if node.typename: with cg.try_squash_raise(): cg.dup_top() cmn.load_helper(cg, 'type_check_expr') cg.rot_two() cmn.load_typename(cg, node.typename, names) cg.call_function(2) cg.pop_top() # Store the result in the fast locals. cg.store_fast(node.name)
class MenuNode(PathNode): """ A path node representing a menu item. This class is an implementation detail and should not be consumed by code outside of this module. """ #: The child objects defined for this menu node. children = List(PathNode) def group_data(self): """ The group map and list of group items for the node. Returns ------- result : tuple A tuple of (dict, list) which holds the mapping of group id to ItemGroup object, and the flat list of ordered groups. """ group_map = {} item_groups = self.item.item_groups for group in item_groups: if group.id in group_map: msg = "menu item '%s' has duplicate group '%s'" raise ValueError(msg % (self.path, group.id)) group_map[group.id] = group if u'' not in group_map: group = ItemGroup() group_map[u''] = group item_groups.append(group) return group_map, item_groups def collect_child_groups(self): """ Yield the ordered and grouped children. """ group_map, item_groups = self.group_data() grouped = defaultdict(list) for child in self.children: target_group = child.item.group if target_group not in group_map: msg = "item '%s' has invalid group '%s'" raise ValueError(msg % (child.path, target_group)) grouped[target_group].append(child) for group in item_groups: if group.id in grouped: nodes = grouped.pop(group.id) yield group, solve_ordering(nodes) def create_children(self, group, nodes): """ Create the child widgets for the given group of nodes. This will assemble the nodes and setup the action groups. """ result = [] actions = [] children = [node.assemble() for node in nodes] def process_actions(): if actions: wag = WorkbenchActionGroup(group=group) wag.insert_children(None, actions) result.append(wag) del actions[:] for child in children: if isinstance(child, WorkbenchAction): actions.append(child) else: process_actions() child.group = group result.append(child) process_actions() return result def assemble_children(self): """ Assemble the list of child objects for the menu. """ children = [] for group, nodes in self.collect_child_groups(): children.extend(self.create_children(group, nodes)) children.append(Action(separator=True)) if children: children.pop() return children def assemble(self): """ Assemble and return a WorkbenchMenu for the node. """ menu = WorkbenchMenu(item=self.item) menu.insert_children(None, self.assemble_children()) return menu
class ListTest(Atom): no_default = List() default = List(default=['a'])
class DevicePlugin(Plugin): """ Plugin for configuring, using, and communicating with a device. """ #: Protocols registered in the system protocols = List(extensions.DeviceProtocol) #: Transports transports = List(extensions.DeviceTransport) #: Drivers registered in the system drivers = List(extensions.DeviceDriver) #: Devices configured devices = List(Device).tag(config=True) #: Current device device = Instance(Device).tag(config=True) # ------------------------------------------------------------------------- # Plugin API # ------------------------------------------------------------------------- def start(self): """ Load all the plugins the device is dependent on """ w = self.workbench plugins = [] with enaml.imports(): from .transports.serialport.manifest import SerialManifest from .transports.printer.manifest import PrinterManifest from .transports.disk.manifest import FileManifest from inkcut.device.protocols.manifest import ProtocolManifest from inkcut.device.drivers.manifest import DriversManifest from inkcut.device.pi.manifest import PiManifest plugins.append(SerialManifest) plugins.append(PrinterManifest) plugins.append(FileManifest) plugins.append(ProtocolManifest) plugins.append(DriversManifest) plugins.append(PiManifest) for Manifest in plugins: w.register(Manifest()) #: This refreshes everything else self._refresh_extensions() #: Restore state after plugins are loaded super(DevicePlugin, self).start() def submit(self, job): """ Send the given job to the device and restart all stats """ job.info.reset() job.info.started = datetime.now() return self.device.submit(job) def _default_device(self): """ If no device is loaded from the previous state, get the device from the first driver loaded. """ self._refresh_extensions() #: If a device is configured, use that if self.devices: return self.devices[0] #: Otherwise create one using the first registered driver if not self.drivers: raise RuntimeError("No device drivers were registered. " "This indicates a missing plugin.") return self.get_device_from_driver(self.drivers[0]) def _observe_device(self, change): """ Whenever the device changes, redraw """ #: Redraw plugin = self.workbench.get_plugin('inkcut.job') plugin.refresh_preview() # ------------------------------------------------------------------------- # Device Driver API # ------------------------------------------------------------------------- def get_device_from_driver(self, driver): """ Load the device driver. This generates the device using the factory function the DeviceDriver specifies and assigns the protocols and transports based on the filters given by the driver. Parameters ---------- driver: inkcut.device.extensions.DeviceDriver The DeviceDriver declaration to use to create a device. Returns ------- device: inkcut.device.plugin.Device The actual device object that will be used for communication and processing the jobs. """ #: Generate the device device = driver.factory(driver.default_config) #: Now set the declaration device.declaration = driver #: Set the protocols based on the declaration if driver.protocols: device.protocols = [ p for p in self.protocols if p.id in driver.protocols ] else: device.protocols = self.protocols[:] #: Set the protocols based on the declaration if driver.connections: device.transports = [ t for t in self.transports if t.id == 'disk' or t.id in driver.connections ] else: device.transports = self.transports[:] return device # ------------------------------------------------------------------------- # Device Extensions API # ------------------------------------------------------------------------- def _refresh_extensions(self): """ Refresh all extensions provided by the DevicePlugin """ self._refresh_protocols() self._refresh_transports() self._refresh_drivers() def _refresh_protocols(self): """ Reload all DeviceProtocols registered by any Plugins Any plugin can add to this list by providing a DeviceProtocol extension in the PluginManifest. """ workbench = self.workbench point = workbench.get_extension_point(extensions.DEVICE_PROTOCOL_POINT) protocols = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for p in extension.get_children(extensions.DeviceProtocol): protocols.append(p) #: Update self.protocols = protocols def _refresh_transports(self): """ Reload all DeviceTransports registered by any Plugins Any plugin can add to this list by providing a DeviceTransport extension in the PluginManifest. """ workbench = self.workbench point = workbench.get_extension_point( extensions.DEVICE_TRANSPORT_POINT) transports = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for t in extension.get_children(extensions.DeviceTransport): transports.append(t) #: Update self.transports = transports def _refresh_drivers(self): """ Reload all DeviceDrivers registered by any Plugins Any plugin can add to this list by providing a DeviceDriver extension in the PluginManifest. """ workbench = self.workbench point = workbench.get_extension_point(extensions.DEVICE_DRIVER_POINT) drivers = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for driver in extension.get_children(extensions.DeviceDriver): if not driver.id: driver.id = "{} {}".format(driver.manufacturer, driver.model) drivers.append(driver) #: Update self.drivers = drivers # ------------------------------------------------------------------------- # Live progress API # ------------------------------------------------------------------------- def reset_preview(self): """ Clear the preview """ self._reset_preview(None) @observe('device', 'device.job') def _reset_preview(self, change): """ Redraw the preview on the screen """ view_items = [] #: Transform used by the view preview_plugin = self.workbench.get_plugin('inkcut.preview') plot = preview_plugin.live_preview t = preview_plugin.transform #: Draw the device device = self.device job = device.job if device and device.area: area = device.area view_items.append( dict(path=device.transform(device.area.path * t), pen=plot.pen_device, skip_autorange=True)) if job and job.material: # Also observe any change to job.media and job.device view_items.extend([ dict(path=device.transform(job.material.path * t), pen=plot.pen_media, skip_autorange=True), dict(path=device.transform(job.material.padding_path * t), pen=plot.pen_media_padding, skip_autorange=True) ]) #: Update the plot preview_plugin.set_live_preview(*view_items) @observe('device.position') def _update_preview(self, change): """ Watch the position of the device as it changes. """ if change['type'] == 'update' and self.device.job: x, y, z = change['value'] preview_plugin = self.workbench.get_plugin('inkcut.preview') preview_plugin.live_preview.update(change['value'])
class EventsResult(Model): Locations = List(Location) Labels = List(Label) Starred = Int()