Source code for pyctools.core.compound

#  Pyctools - a picture processing algorithm development kit.
#  http://github.com/jim-easterbrook/pyctools
#  Copyright (C) 2014-23  Pyctools contributors
#
#  This program is free software: you can redistribute it and/or
#  modify it under the terms of the GNU General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see
#  <http://www.gnu.org/licenses/>.

__all__ = ['Compound', 'RunnableNetwork']
__docformat__ = 'restructuredtext en'

import logging

from .config import ConfigParent


[docs] class RunnableNetwork(object): """Encapsulate several components into one. This is the basic runnable network part of a :py:class:`Compound` component. :keyword Component name: Add ``Component`` to the network as ``name``. Can be repeated with different values of ``name``. :keyword dict linkages: A mapping from component outputs to component inputs. """ inputs = [] #: outputs = [] #: children = {} #: links = [] #: def __init__(self, linkages={}, **kw): super(RunnableNetwork, self).__init__() self.logger = logging.getLogger(self.__class__.__name__) self.inputs = [] self.outputs = [] # get child components self.children = kw # set up linkages self._compound_outputs = {} self.links = [] for source, targets in linkages.items(): if isinstance(targets[0], str): # not a list of pairs, so make it into one targets = list(zip(targets[0::2], targets[1::2])) src, outbox = source for dest, inbox in targets: if src == 'self': if hasattr(self, outbox): self.logger.critical( 'cannot link (%s, %s) to more than one target', src, outbox) setattr(self, outbox, getattr(self.children[dest], inbox)) self.inputs.append(outbox) elif dest == 'self': self._compound_outputs[inbox] = (src, outbox) self.outputs.append(inbox) else: self.children[src].connect( outbox, getattr(self.children[dest], inbox)) self.links.append(((src, outbox), (dest, inbox)))
[docs] def go(self): """Guild compatible version of :py:meth:`start`.""" self.start() return self
[docs] def start(self): """Start the component running.""" for name, child in self.children.items(): self.logger.debug('start %s (%s)', name, child.__class__.__name__) child.start()
[docs] def stop(self): """Thread-safe method to stop the component.""" for name, child in self.children.items(): self.logger.debug('stop %s (%s)', name, child.__class__.__name__) child.stop()
[docs] def join(self, end_comps=False): """Wait for the compound component's children to stop running. :param bool end_comps: only wait for the components that end a pipeline. This is useful for complex graphs where it is normal for some components not to terminate. """ for name, child in self.children.items(): if end_comps and not child.is_pipe_end(): continue self.logger.debug('join %s (%s)', name, child.__class__.__name__) child.join()
def is_pipe_end(self): for src, outbox in self._compound_outputs.values(): if not self.children[src].is_pipe_end(): return False return True
[docs] class Compound(RunnableNetwork): """Encapsulate several components into one. Closely modeled on `Kamaelia's 'Graphline' component <http://www.kamaelia.org/Components/pydoc/Kamaelia.Chassis.Graphline.html>`_. Components are linked within the compound and to the outside world according to the ``linkages`` parameter. For example, you could create an image resizer by connecting a :py:class:`~pyctools.components.interp.filtergenerator.FilterGenerator` to a :py:class:`~pyctools.components.interp.resize.Resize` as follows:: def ImageResizer(config={}, **kwds): cfg = {'aperture': 16} cfg.update(config) cfg.update(kwds) return Compound( filgen = FilterGenerator(), resize = Resize(), config = cfg, config_map = { 'up' : ('filgen.xup', 'resize.xup', 'filgen.yup', 'resize.yup'), 'down' : ('filgen.xdown', 'resize.xdown', 'filgen.ydown', 'resize.ydown'), 'aperture': ('filgen.xaperture', 'filgen.yaperture'), }, linkages = { ('self', 'input') : ('resize', 'input'), ('filgen', 'output') : ('resize', 'filter'), ('resize', 'output') : ('self', 'output'), } ) Note the use of ``'self'`` in the ``linkages`` parameter to denote the compound object's own inputs and outputs. These are connected directly to the child components with no runtime overhead. There is no performance disadvantage from using compound objects. The ``'self'`` inboxes and outboxes are added to the component's :py:attr:`~Compound.inputs` and :py:attr:`~Compound.outputs` lists. All the child components' configuration objects are gathered into one :py:class:`~.config.ConfigParent`. The child names are used to index the :py:class:`~.config.ConfigParent`'s dict. This allows access to any config item in any child:: cfg = image_resizer.get_config() cfg.filgen.xup = 3 cfg.filgen.xdown = 8 cfg.filgen.yup = 3 cfg.filgen.ydown = 8 cfg.resize.xup = 3 cfg.resize.xdown = 8 cfg.resize.yup = 3 cfg.resize.ydown = 8 image_resizer.set_config(cfg) Compound components to be nested to any depth whilst still making their configuration available at the top level. The ``config_map`` allows multiple child components to be controlled by one configuration item. For each item there is a list of child config items, in parent.child form. For example, to change the scaling factor of the image resizer shown above (even while it's running!) you might do this:: cfg = image_resizer.get_config() cfg.up = 3 cfg.down = 8 image_resizer.set_config(cfg) You can also adjust the configuration when the compound component is created by passing a :py:class:`dict` containing additional values. This allows the component's user to over-ride the default values. The compound component's child components are stored in the :py:attr:`~Compound.children` dict. This must not be modified but may be useful if you need to know about the component's internals. The component's internal links are stored in the :py:attr:`~Compound.links` list. This also must not be modified but can be used for introspection. Each element is a ``(src_name, outbox), (dest_name, inbox)`` tuple. :keyword Component name: Add ``Component`` to the network as ``name``. Can be repeated with different values of ``name``. :keyword dict linkages: A mapping from component outputs to component inputs. :keyword dict config: Additional configuration to be applied to the components before they are connected. :keyword dict config_map: Mapping of top level configuration names to child component configuration names. """ def __init__(self, config={}, config_map={}, linkages={}, **kw): super(Compound, self).__init__(linkages=linkages, **kw) # set config self.config = ConfigParent(config_map=config_map) for name, child in self.children.items(): self.config[name] = child.get_config() self.config.set_default(config=config)
[docs] def connect(self, output_name, input_method): """Connect an output to any callable object. :param str output_name: the output to connect. Must be one of the ``'self'`` outputs in the ``linkages`` parameter. :param callable input_method: the thread-safe callable to invoke when :py:meth:`send` is called. """ src, outbox = self._compound_outputs[output_name] self.children[src].connect(outbox, input_method)
[docs] def bind(self, source, dest, destmeth): """Guild compatible version of :py:meth:`connect`. This allows Pyctools compound components to be used in `Guild <https://github.com/sparkslabs/guild>`_ pipelines. """ self.connect(source, getattr(dest, destmeth))
[docs] def get_config(self): """See :py:meth:`pyctools.core.config.ConfigMixin.get_config`.""" return self.config.copy()
[docs] def set_config(self, config): """See :py:meth:`pyctools.core.config.ConfigMixin.set_config`.""" self.config.update(config) for name, child in self.children.items(): child.set_config(self.config[name])