Exemplo n.º 1
0
class Settings(EntityBase, EntryHDF5IOMixin):

    DEFAULT_FILENAME = "settings.h5"

    settings_changed = Signal()

    def __init__(self):
        # Units
        self.preferred_units = {}

        # X-ray line
        self.preferred_xray_notation = XrayNotation.IUPAC

        # Paths
        self._opendir = None
        self._savedir = None

    @classmethod
    def read(cls, filepath=None):
        if filepath is None:
            filepath = os.path.join(get_config_dir(), cls.DEFAULT_FILENAME)
            if not os.path.exists(filepath):
                return cls()

        return super().read(filepath)

    def write(self, filepath=None):
        if filepath is None:
            filepath = os.path.join(get_config_dir(), self.DEFAULT_FILENAME)
        return super().write(filepath)

    def set_preferred_unit(self, units):
        if isinstance(units, str):
            units = pymontecarlo.unit_registry.parse_units(units)

        _, base_units = pymontecarlo.unit_registry._get_base_units(units)
        self.preferred_units[base_units] = units

    def clear_preferred_units(self):
        self.preferred_units.clear()

    def to_preferred_unit(self, q, units=None):
        if not hasattr(q, "units"):
            q = pymontecarlo.unit_registry.Quantity(q, units)

        _, base_unit = pymontecarlo.unit_registry._get_base_units(q.units)

        try:
            preferred_unit = self.preferred_units[base_unit]
            return q.to(preferred_unit)
        except KeyError:
            return q.to(base_unit)

    @property
    def opendir(self):
        return self._opendir or self._savedir or os.getcwd()

    @opendir.setter
    def opendir(self, dirpath):
        self._opendir = dirpath

    @property
    def savedir(self):
        return self._savedir or self._opendir or os.getcwd()

    @savedir.setter
    def savedir(self, dirpath):
        self._savedir = dirpath

    # region HDF5

    DATASET_PREFERRED_UNITS = "preferred units"
    ATTR_PREFERRED_XRAY_NOTATION = "preferred x-ray notation"
    ATTR_OPENDIR = "opendir"
    ATTR_SAVEDIR = "savedir"

    @classmethod
    def parse_hdf5(cls, group):
        obj = cls()

        units = [str(value) for value in group[cls.DATASET_PREFERRED_UNITS].asstr()]
        for unit in units:
            obj.set_preferred_unit(unit)

        obj.preferred_xray_notation = cls._parse_hdf5(
            group, cls.ATTR_PREFERRED_XRAY_NOTATION, XrayNotation
        )
        obj.opendir = cls._parse_hdf5(group, cls.ATTR_OPENDIR, str)
        obj.savedir = cls._parse_hdf5(group, cls.ATTR_SAVEDIR, str)

        return obj

    def convert_hdf5(self, group):
        super().convert_hdf5(group)

        shape = (len(self.preferred_units),)
        dtype = h5py.special_dtype(vlen=str)
        dataset = group.create_dataset(self.DATASET_PREFERRED_UNITS, shape, dtype)
        dataset[:] = list(map(str, self.preferred_units.values()))

        self._convert_hdf5(
            group, self.ATTR_PREFERRED_XRAY_NOTATION, self.preferred_xray_notation
        )
        self._convert_hdf5(group, self.ATTR_OPENDIR, self.opendir)
        self._convert_hdf5(group, self.ATTR_SAVEDIR, self.savedir)
Exemplo n.º 2
0
class Project(EntityBase, EntryHDF5IOMixin):

    simulation_added = Signal()
    simulation_recalculated = Signal()

    def __init__(self, filepath=None):
        self.filepath = filepath
        self.simulations = []
        self.lock = threading.Lock()
        self.recalculate_required = False

    def __getstate__(self):
        with self.lock:
            return (self.filepath, self.simulations)

    def __setstate__(self, state):
        filepath, simulations = state

        self.filepath = filepath
        self.simulations = simulations
        self.recalculate_required = True

    def add_simulation(self, simulation):
        with self.lock:
            if simulation in self.simulations:
                return

            identifiers = [
                s.identifier for s in self.simulations
                if s.identifier.startswith(simulation.identifier)
            ]
            if identifiers:
                last = -1
                for identifier in identifiers:
                    m = re.search(r"-(\d+)$", identifier)
                    if m is not None:
                        last = max(last, int(m.group(1)))

                simulation.identifier += "-{:d}".format(last + 1)

            self.simulations.append(simulation)
            self.recalculate_required = True
            self.simulation_added.send(simulation)

    async def recalculate(self, token=None):
        with self.lock:
            if token:
                token.start()
            count = len(self.simulations)

            for i, simulation in enumerate(self.simulations):
                progress = i / count
                status = "Calculating simulation {}".format(
                    simulation.identifier)
                if token:
                    token.update(progress, status)

                newresult = False
                for analysis in simulation.options.analyses:
                    newresult |= analysis.calculate(simulation,
                                                    tuple(self.simulations))

                if newresult:
                    self.simulation_recalculated.send(simulation)

            if token:
                token.done()

            self.recalculate_required = False

    def create_options_dataframe(
        self,
        settings,
        only_different_columns=False,
        abbreviate_name=False,
        format_number=False,
    ):
        """
        Returns a :class:`pandas.DataFrame`.

        If *only_different_columns*, the data rows will only contain the columns
        that are different between the options.
        """
        list_options = [simulation.options for simulation in self.simulations]
        return create_options_dataframe(
            list_options,
            settings,
            only_different_columns,
            abbreviate_name,
            format_number,
        )

    def create_results_dataframe(self,
                                 settings,
                                 result_classes=None,
                                 abbreviate_name=False,
                                 format_number=False):
        """
        Returns a :class:`pandas.DataFrame`.

        If *result_classes* is a list of :class:`Result`, only the columns from
        this result classes will be returned. If ``None``, the columns from
        all results will be returned.
        """
        list_results = [simulation.results for simulation in self.simulations]
        return create_results_dataframe(list_results, settings, result_classes,
                                        abbreviate_name, format_number)

    def create_dataframe(
        self,
        settings,
        only_different_columns=False,
        abbreviate_name=False,
        format_number=False,
        result_classes=None,
    ):
        """
        Returns a :class:`pandas.DataFrame`, combining the :class:`pandas.DataFrame` created
        by :meth:`.create_options_dataframe` and :meth:`.create_results_dataframe`.
        """
        df_options = self.create_options_dataframe(settings,
                                                   only_different_columns,
                                                   abbreviate_name,
                                                   format_number)
        df_results = self.create_results_dataframe(settings, result_classes,
                                                   abbreviate_name,
                                                   format_number)

        return pd.concat([df_options, df_results], axis=1)

    def write(self, filepath=None):
        if filepath is None:
            filepath = self.filepath
        if filepath is None:
            raise RuntimeError("No file path given")
        super().write(filepath)

    @property
    def result_classes(self):
        """
        Returns all types of result.
        """
        classes = set()

        for simulation in self.simulations:
            classes.update(type(result) for result in simulation.results)

        return classes

    # region HDF5

    GROUP_SIMULATIONS = "simulations"

    @classmethod
    def parse_hdf5(cls, group):
        filepath = group.file.filename
        project = cls(filepath)

        simulations = [
            cls._parse_hdf5_object(group_simulation)
            for group_simulation in group[cls.GROUP_SIMULATIONS].values()
        ]
        with project.lock:
            project.simulations.extend(simulations)

        return project

    def convert_hdf5(self, group):
        super().convert_hdf5(group)

        group_simulations = group.create_group(self.GROUP_SIMULATIONS)

        with self.lock:
            for simulation in self.simulations:
                name = simulation.identifier
                group_simulation = group_simulations.create_group(name)
                simulation.convert_hdf5(group_simulation)
Exemplo n.º 3
0
class FutureExecutor(Monitorable):

    submitted = Signal()

    def __init__(self, max_workers=1):
        self.max_workers = max_workers
        self.executor = None
        self.futures = set()
        self.failed_futures = set()

        self.failed_count = 0
        self.cancelled_count = 0
        self.submitted_count = 0
        self.done_count = 0

    def __enter__(self):
        self.start()
        return self

    def __exit__(self, exctype, value, tb):
        self.shutdown()
        return False

    def _on_done(self, future):
        if future.cancelled():
            future.token.update(1.0, 'Cancelled')
            self.cancelled_count += 1
            return

        if future.exception():
            future.token.update(1.0, 'Error')
            self.failed_futures.add(future)
            self.failed_count += 1
            return

        future.token.update(1.0, 'Done')
        self.done_count += 1
        return future.result()

    def start(self):
        if self.executor is not None:
            return
        self.executor = concurrent.futures.ThreadPoolExecutor(self.max_workers)

    def cancel(self):
        """
        Cancels all not completed futures.
        """
        for future in self.futures:
            if not future.done():
                future.cancel()

    def shutdown(self):
        if self.executor is None:
            return
        self.executor.shutdown(wait=True)
        self.futures.clear()

    def wait(self, timeout=None):
        """
        Waits forever if *timeout* is ``None``. 
        Otherwise waits for *timeout* and returns ``True`` if all submissions
        were executed, ``False`` otherwise.
        """
        fs = [future.future for future in self.futures]
        _done, notdone = \
            concurrent.futures.wait(fs, timeout, concurrent.futures.ALL_COMPLETED)
        return not notdone

    def _submit(self, target, *args, **kwargs):
        """
        Submits target function with specified arguments.
        
        .. note:: The derived class should ideally create a :meth:`submit` 
            method that calls this method.
        
        :arg target: function to execute. The first argument of the function
            should be a token, where the progress, status of the function
            can be updated::
            
            def target(token):
                token.update(0.0, 'start')
                if token.cancelled():
                    return
                token.update(1.0, 'done')
        
        :return: a :class:`Future` object
        """
        if self.executor is None:
            raise RuntimeError('Executor is not started')

        token = Token()
        future = self.executor.submit(target, token, *args, **kwargs)

        future2 = FutureAdapter(future, token, args, kwargs)
        future2.add_done_callback(self._on_done)
        self.futures.add(future2)

        self.submitted_count += 1
        self.submitted.send(future2)

        return future2

    def running(self):
        """
        Returns whether the executor is running and can accept submission.
        """
        return any(future.running() for future in self.futures)

    def done(self):
        return all(future.done() for future in self.futures)

    def cancelled(self):
        return False

    @property
    def progress(self):
        if self.submitted_count == 0:
            return 0
        return (self.done_count + self.failed_count +
                self.cancelled_count) / self.submitted_count

    @property
    def status(self):
        return ''
Exemplo n.º 4
0
class Settings(HDF5ReaderMixin, HDF5WriterMixin):

    DEFAULT_FILENAME = 'settings.h5'

    activated_programs_changed = Signal()
    preferred_units_changed = Signal()
    preferred_xrayline_notation_changed = Signal()
    preferred_xrayline_encoding_changed = Signal()

    def __init__(self):
        # Programs
        self._activated_programs = {}  # key: identifier, value: program object
        self._available_programs = {}  # key: identifier, value: program class

        # Units
        self.preferred_units = {}

        # X-ray line
        self._preferred_xrayline_notation = 'iupac'
        self._preferred_xrayline_encoding = 'utf16'

    @classmethod
    def read(cls, filepath=None):
        if filepath is None:
            filepath = os.path.join(get_config_dir(), cls.DEFAULT_FILENAME)
        return super().read(filepath)
#

    def write(self, filepath=None):
        if filepath is None:
            filepath = os.path.join(get_config_dir(), self.DEFAULT_FILENAME)
        return super().write(filepath)

    def _validate(self, errors):
        # Programs
        for program in self.activated_programs:
            validator = program.create_validator()
            validator._validate_program(program, None, errors)

    def validate(self):
        errors = set()
        self._validate(errors)

        if errors:
            raise ValidationError(*errors)

    def update(self, settings):
        settings.validate()

        self._activated_programs.clear()
        self._activated_programs = settings._activated_programs.copy()

        self.preferred_units.clear()
        self.preferred_units.update(settings.preferred_units)
        self.preferred_units_changed.send()

        self.preferred_xrayline_notation = settings.preferred_xrayline_notation
        self.preferred_xrayline_encoding = settings.preferred_xrayline_encoding

    def reload(self):
        self._available_programs.clear()
        entrypoint._ENTRYPOINTS.clear()

    def get_activated_program(self, identifier):
        """
        Returns the :class:`Program` matching the specified identifier.
        """
        try:
            return self._activated_programs[identifier]
        except KeyError:
            raise ProgramNotFound('{} is not configured'.format(identifier))

    def get_available_program_class(self, identifier):
        """
        Returns the :class:`Program` class matching the specified identifier.
        """
        try:
            self.available_programs  # Initialize
            return self._available_programs[identifier]
        except KeyError:
            raise ProgramNotFound('{} is not available'.format(identifier))

    def is_program_activated(self, identifier):
        return identifier in self._activated_programs

    def is_program_available(self, identifier):
        return identifier in self._available_programs

    def activate_program(self, program):
        identifier = program.getidentifier()

        if self.is_program_activated(identifier):
            raise ValueError('{} is already activated'.format(identifier))

        self._activated_programs[identifier] = program
        self.activated_programs_changed.send()

    def deactivate_program(self, identifier):
        self._activated_programs.pop(identifier, None)
        self.activated_programs_changed.send()

    def deactivate_all_programs(self):
        self._activated_programs.clear()
        self.activated_programs_changed.send()

    def set_preferred_unit(self, units, quiet=False):
        if isinstance(units, str):
            units = pymontecarlo.unit_registry.parse_units(units)

        _, base_units = pymontecarlo.unit_registry._get_base_units(units)
        self.preferred_units[base_units] = units

        if not quiet:
            self.preferred_units_changed.send()

    def clear_preferred_units(self, quiet=False):
        self.preferred_units.clear()

        if not quiet:
            self.preferred_units_changed.send()

    def to_preferred_unit(self, q, units=None):
        if not hasattr(q, 'units'):
            q = pymontecarlo.unit_registry.Quantity(q, units)

        _, base_unit = pymontecarlo.unit_registry._get_base_units(q.units)

        try:
            preferred_unit = self.preferred_units[base_unit]
            return q.to(preferred_unit)
        except KeyError:
            return q.to(base_unit)

    @property
    def activated_programs(self):
        """
        Returns a :class:`tuple` of all activated programs. 
        The items are :class:`Program` instances.
        """
        return tuple(self._activated_programs.values())

    @property
    def available_programs(self):
        """
        Returns a :class:`tuple` of all available programs, whether or not
        they are activated.
        The items are :class:`Program` classes.
        """
        # Late initialization
        if not self._available_programs:
            self._available_programs = {}

            for clasz in entrypoint.resolve_entrypoints(
                    ENTRYPOINT_AVAILABLE_PROGRAMS):
                identifier = clasz.getidentifier()
                self._available_programs[identifier] = clasz

        return tuple(self._available_programs.values())

    @property
    def preferred_xrayline_notation(self):
        return self._preferred_xrayline_notation

    @preferred_xrayline_notation.setter
    def preferred_xrayline_notation(self, notation):
        if self._preferred_xrayline_notation == notation:
            return
        self._preferred_xrayline_notation = notation
        self.preferred_xrayline_notation_changed.send()

    @property
    def preferred_xrayline_encoding(self):
        return self._preferred_xrayline_encoding

    @preferred_xrayline_encoding.setter
    def preferred_xrayline_encoding(self, encoding):
        if self._preferred_xrayline_encoding == encoding:
            return
        self._preferred_xrayline_encoding = encoding
        self.preferred_xrayline_encoding_changed.send()
Exemplo n.º 5
0
class Project(HDF5ReaderMixin, HDF5WriterMixin):

    simulation_added = Signal()
    recalculated = Signal()

    def __init__(self, filepath=None):
        self.filepath = filepath
        self.simulations = []
        self.lock = threading.Lock()
        self.recalculate_required = False

    def add_simulation(self, simulation):
        with self.lock:
            if simulation in self.simulations:
                return

            identifiers = [
                s.identifier for s in self.simulations
                if s.identifier.startswith(simulation.identifier)
            ]
            if identifiers:
                last = -1
                for identifier in identifiers:
                    m = re.search(r'-(\d+)$', identifier)
                    if m is not None:
                        last = max(last, int(m.group(1)))

                simulation.identifier += '-{:d}'.format(last + 1)

            self.simulations.append(simulation)
            self.recalculate_required = True
            self.simulation_added.send(simulation)

    def recalculate(self, token=None):
        with self.lock:
            count = len(self.simulations)

            for i, simulation in enumerate(self.simulations):
                if token and token.cancelled():
                    break

                progress = i / count
                status = 'Calculating simulation {}'.format(
                    simulation.identifier)
                if token: token.update(progress, status)

                for analysis in simulation.options.analyses:
                    analysis.calculate(simulation, tuple(self.simulations))

            if token: token.update(1.0, 'Done')

            self.recalculate_required = False
            self.recalculated.send()

    def create_options_dataframe(self, only_different_columns=False):
        """
        Returns a :class:`pandas.DataFrame`.
        
        If *only_different_columns*, the data rows will only contain the columns
        that are different between the options.
        """
        list_options = [simulation.options for simulation in self.simulations]
        return create_options_dataframe(list_options, only_different_columns)

    def create_results_dataframe(self, result_classes=None):
        """
        Returns a :class:`pandas.DataFrame`.
        
        If *result_classes* is a list of :class:`Result`, only the columns from
        this result classes will be returned. If ``None``, the columns from 
        all results will be returned.
        """
        list_results = [simulation.results for simulation in self.simulations]
        return create_results_dataframe(list_results, result_classes)

    def write(self, filepath=None):
        if filepath is None:
            filepath = self.filepath
        if filepath is None:
            raise RuntimeError('No file path given')
        super().write(filepath)

    @property
    def result_classes(self):
        """
        Returns all types of result.
        """
        classes = set()

        for simulation in self.simulations:
            classes.update(type(result) for result in simulation.results)

        return classes