Ejemplo n.º 1
0
class _DummyObject(DataModel):

    name = StringProperty(text="Name", tabular=True, default="")
    number = FloatProperty(text="Number", tabular=True, default=0)
    test = LabeledProperty(text="Test", tabular=True, default=[])

    pass  # end of class
Ejemplo n.º 2
0
class InSituBehaviour(DataModel, RefinementGroup, Storable):
    """
        Interface class for coding in-situ behaviour scripts.
        Sub-classes should override or implement the methods below.
    """

    # MODEL INTEL:
    class Meta(DataModel.Meta):
        store_id = "InSituBehaviour"  # Override this so it is a unique string
        concrete = False  # Indicates this cannot be instantiated and added in the UI

    mixture = property(DataModel.parent.fget, DataModel.parent.fset)

    # REFINEMENT GROUP IMPLEMENTATION:
    @property
    def refine_title(self):
        return "In-situ behaviour"

    @property
    def refine_descriptor_data(self):
        return dict(phase_name=self.phase.refine_title, component_name="*")

    #: The name of this Behaviour
    name = StringProperty(default="New Behaviour",
                          text="Name",
                          visible=True,
                          persistent=True,
                          tabular=True)

    # ------------------------------------------------------------
    #      Initialization and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        my_kwargs = self.pop_kwargs(
            kwargs, *[
                prop.label for prop in
                InSituBehaviour.Meta.get_local_persistent_properties()
            ])
        super(InSituBehaviour, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        with self.data_changed.hold():
            self.name = self.get_kwarg(kwargs, self.name, "name")

        pass  #end of constructor

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def apply(self, phase):
        assert phase is not None, "Cannot apply on None"
        assert self.is_compatible_with(
            phase), "`%r` is not compatible with phase `%r`" % (self, phase)

    def is_compatible_with(self, phase):
        return False  # sub classes need to override this

    pass  #end of class
Ejemplo n.º 3
0
class AbstractPhase(DataModel, Storable):

    # MODEL INTEL:
    class Meta(DataModel.Meta):
        store_id = "AbstractPhase"

    _data_object = None

    @property
    def data_object(self):
        return self._data_object

    project = property(DataModel.parent.fget, DataModel.parent.fset)

    # PROPERTIES:

    #: The name of this Phase
    name = StringProperty(
        default="New Phase",
        text="Name",
        visible=True,
        persistent=True,
        tabular=True,
    )

    #: The # of components
    @IntegerProperty(default=0,
                     text="# of components",
                     visible=True,
                     persistent=True,
                     tabular=True,
                     widget_type="entry",
                     mix_with=(ReadOnlyMixin, ))
    def G(self):
        return 0

    #: The Reichweite
    @IntegerProperty(default=0,
                     text="Reichweite",
                     visible=True,
                     persistent=False,
                     tabular=True,
                     widget_type="entry",
                     mix_with=(ReadOnlyMixin, ))
    def R(self):
        return 0

    #: The color this phase's X-ray diffraction pattern should have.
    display_color = StringProperty(default="#FFB600",
                                   text="Display color",
                                   visible=True,
                                   persistent=True,
                                   tabular=True,
                                   widget_type='color',
                                   signal_name="visuals_changed",
                                   mix_with=(SignalMixin, ))

    line_colors = [
        "#004488",
        "#FF4400",
        "#559911",
        "#770022",
        "#AACC00",
        "#441177",
    ]

    # ------------------------------------------------------------
    #      Initialization and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):

        my_kwargs = self.pop_kwargs(
            kwargs, "data_name", "data_G", "data_R", *[
                prop.label for prop in
                AbstractPhase.Meta.get_local_persistent_properties()
            ])
        super(AbstractPhase, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        with self.data_changed.hold():

            self._data_object = PhaseData()

            self.name = self.get_kwarg(kwargs, self.name, "name", "data_name")
            self.display_color = self.get_kwarg(kwargs,
                                                choice(self.line_colors),
                                                "display_color")

    def __repr__(self):
        return "AbstractPhase(name='%s')" % (self.name)

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    def resolve_json_references(self):
        pass  # nothing to do, sub-classes should override

    def _pre_multi_save(self, phases, ordered_phases):
        pass  # nothing to do, sub-classes should override

    def _post_multi_save(self):
        pass  # nothing to do, sub-classes should override

    @classmethod
    def save_phases(cls, phases, filename):
        """
            Saves multiple phases to a single file.
        """
        ordered_phases = list(phases)  # make a copy
        for phase in phases:
            phase._pre_multi_save(phases, ordered_phases)

        with zipfile.ZipFile(filename, 'w', compression=COMPRESSION) as zfile:
            for i, phase in enumerate(ordered_phases):
                zfile.writestr("%d###%s" % (i, phase.uuid),
                               phase.dump_object())

        for phase in ordered_phases:
            phase._post_multi_save()

        # After export we change all the UUID's
        # This way, we're sure that we're not going to import objects with
        # duplicate UUID's!
        type(cls).object_pool.change_all_uuids()

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def get_diffracted_intensity(self, range_theta, range_stl, *args):
        return get_diffracted_intensity(range_theta, range_stl,
                                        self.data_object)

    pass  # end of class
Ejemplo n.º 4
0
class PyXRDLine(StorableXYData):
    """
        A PyXRDLine is an abstract attribute holder for a real 'Line' object,
        whatever the plotting library used may be. Attributes are line width and
        color.        
    """

    # MODEL INTEL:
    class Meta(StorableXYData.Meta):
        store_id = "PyXRDLine"

    # OBSERVABLE PROPERTIES:

    #: The line label
    label = StringProperty(default="", text="Label", persistent=True)

    #: The line color
    color = StringProperty(default="#000000",
                           text="Label",
                           visible=True,
                           persistent=True,
                           widget_type="color",
                           inherit_flag="inherit_color",
                           inherit_from="parent.parent.display_exp_color",
                           signal_name="visuals_changed",
                           mix_with=(InheritableMixin, SignalMixin))

    #: Flag indicating whether to use the grandparents color yes/no
    inherit_color = BoolProperty(default=True,
                                 text="Inherit color",
                                 visible=True,
                                 persistent=True,
                                 signal_name="visuals_changed",
                                 mix_with=(SignalMixin, ))

    #: The linewidth in points
    lw = FloatProperty(
        default=2.0,
        text="Linewidth",
        visible=True,
        persistent=True,
        widget_type="spin",
        inherit_flag="inherit_lw",
        inherit_from="parent.parent.display_exp_lw",
        signal_name="visuals_changed",
        mix_with=(InheritableMixin, SignalMixin),
    )

    #: Flag indicating whether to use the grandparents linewidth yes/no
    inherit_lw = BoolProperty(
        default=True,
        text="Inherit linewidth",
        visible=True,
        persistent=True,
        signal_name="visuals_changed",
        mix_with=(SignalMixin, ),
    )

    #: A short string describing the (matplotlib) linestyle
    ls = StringChoiceProperty(
        default=settings.EXPERIMENTAL_LINESTYLE,
        text="Linestyle",
        visible=True,
        persistent=True,
        choices=settings.PATTERN_LINE_STYLES,
        mix_with=(
            InheritableMixin,
            SignalMixin,
        ),
        signal_name="visuals_changed",
        inherit_flag="inherit_ls",
        inherit_from="parent.parent.display_exp_ls",
    )

    #: Flag indicating whether to use the grandparents linestyle yes/no
    inherit_ls = BoolProperty(default=True,
                              text="Inherit linestyle",
                              visible=True,
                              persistent=True,
                              mix_with=(SignalMixin, ),
                              signal_name="visuals_changed")

    #: A short string describing the (matplotlib) marker
    marker = StringChoiceProperty(
        default=settings.EXPERIMENTAL_MARKER,
        text="Marker",
        visible=True,
        persistent=True,
        choices=settings.PATTERN_MARKERS,
        mix_with=(
            InheritableMixin,
            SignalMixin,
        ),
        signal_name="visuals_changed",
        inherit_flag="inherit_marker",
        inherit_from="parent.parent.display_exp_marker",
    )

    #: Flag indicating whether to use the grandparents linewidth yes/no
    inherit_marker = BoolProperty(
        default=True,
        text="Inherit marker",
        visible=True,
        persistent=True,
        mix_with=(SignalMixin, ),
        signal_name="visuals_changed",
    )

    #: z-data (e.g. relative humidity, temperature, for multi-column 'lines')
    z_data = ListProperty(default=None,
                          text="Z data",
                          data_type=float,
                          persistent=True,
                          visible=False)

    # REGULAR PROPERTIES:
    @property
    def max_display_y(self):
        if self.num_columns > 2:
            # If there's several y-columns, check if we have z-data associated with them
            # if so, it is a 2D pattern, otherwise this is a multi-line pattern
            if len(self.z_data) > 2:
                return np.max(self.z_data)
            else:
                return self.max_y
        else:
            # If there's a single comumn of y-data, just get the max value
            return self.max_y

    @property
    def min_intensity(self):
        if self.num_columns > 2:
            return np.min(self.z_data)
        else:
            return self.min_y

    @property
    def abs_max_intensity(self):
        return self.abs_max_y

    # ------------------------------------------------------------
    #      Initialisation and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        """
            Valid keyword arguments for a PyXRDLine are:
                data: the actual data containing x and y values
                label: the label for this line
                color: the color of this line
                inherit_color: whether to use the parent-level color or its own
                lw: the line width of this line
                inherit_lw: whether to use the parent-level line width or its own
                ls: the line style of this line
                inherit_ls: whether to use the parent-level line style or its own
                marker: the line marker of this line
                inherit_marker: whether to use the parent-level line marker or its own
                z_data: the z-data associated with the columns in a multi-column pattern
        """
        my_kwargs = self.pop_kwargs(
            kwargs, *[
                prop.label
                for prop in PyXRDLine.Meta.get_local_persistent_properties()
            ])
        super(PyXRDLine, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        with self.visuals_changed.hold():
            self.label = self.get_kwarg(kwargs, self.label, "label")
            self.color = self.get_kwarg(kwargs, self.color, "color")
            self.inherit_color = bool(
                self.get_kwarg(kwargs, self.inherit_color, "inherit_color"))
            self.lw = float(self.get_kwarg(kwargs, self.lw, "lw"))
            self.inherit_lw = bool(
                self.get_kwarg(kwargs, self.inherit_lw, "inherit_lw"))
            self.ls = self.get_kwarg(kwargs, self.ls, "ls")
            self.inherit_ls = bool(
                self.get_kwarg(kwargs, self.inherit_ls, "inherit_ls"))
            self.marker = self.get_kwarg(kwargs, self.marker, "marker")
            self.inherit_marker = bool(
                self.get_kwarg(kwargs, self.inherit_marker, "inherit_marker"))
            self.z_data = list(self.get_kwarg(kwargs, [0], "z_data"))

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    @classmethod
    def from_json(cls, **kwargs):  # @ReservedAssignment
        if "xy_store" in kwargs:
            if "type" in kwargs["xy_store"]:
                kwargs["data"] = kwargs["xy_store"]["properties"]["data"]
        elif "xy_data" in kwargs:
            if "type" in kwargs["xy_data"]:
                kwargs["data"] = kwargs["xy_data"]["properties"]["data"]
            kwargs["label"] = kwargs["data_label"]
            del kwargs["data_name"]
            del kwargs["data_label"]
            del kwargs["xy_data"]
        return cls(**kwargs)

    # ------------------------------------------------------------
    #      Convenience Methods & Functions
    # ------------------------------------------------------------
    def interpolate(self, *x_vals, **kwargs):
        """
            Returns a list of (x, y) tuples for the passed x values. An optional
            column keyword argument can be passed to select a column, by default
            the first y-column is used. Returned y-values are interpolated. 
        """
        column = kwargs.get("column", 0)
        f = interp1d(self.data_x, self.data_y[:, column])
        return list(zip(x_vals, f(x_vals)))

    def get_plotted_y_at_x(self, x):
        """
            Gets the (interpolated) plotted value at the given x position.
            If this line has not been plotted (or does not have
            access to a '__plot_line' attribute set by the plotting routines)
            it will return 0.
        """
        try:
            xdata, ydata = getattr(self, "__plot_line").get_data()
        except AttributeError:
            logging.exception(
                "Attribute error when trying to get plotter data at x position!"
            )
        else:
            if len(xdata) > 0 and len(ydata) > 0:
                return np.interp(x, xdata, ydata)
        return 0

    def calculate_npeaks_for(self, max_threshold, steps):
        """
            Calculates the number of peaks for `steps` threshold values between
            0 and `max_threshold`. Returns a tuple containing two lists with the
            threshold values and the corresponding number of peaks. 
        """
        length = self.data_x.size

        resolution = length / (self.data_x[-1] - self.data_x[0])
        delta_angle = 0.05
        window = int(delta_angle * resolution)
        window += (window % 2) * 2

        steps = max(steps, 2) - 1
        factor = max_threshold / steps

        deltas = [i * factor for i in range(0, steps)]

        numpeaks = []

        maxtabs, mintabs = multi_peakdetect(self.data_y[:, 0], self.data_x, 5,
                                            deltas)
        for maxtab, _ in zip(maxtabs, mintabs):
            numpeak = len(maxtab)
            numpeaks.append(numpeak)
        numpeaks = list(map(float, numpeaks))

        return deltas, numpeaks

    def get_best_threshold(self,
                           max_threshold=None,
                           steps=None,
                           status_dict=None):
        """
            Estimates the best threshold for peak detection using an
            iterative algorithm. Assumes there is a linear contribution from noise.
            Returns a 4-tuple containing the selected threshold, the maximum
            threshold, a list of threshold values and a list with the corresponding
            number of peaks.
        """
        length = self.data_x.size
        steps = not_none(steps, 20)
        threshold = 0.1
        max_threshold = not_none(max_threshold, threshold * 3.2)

        def get_new_threshold(threshold, deltas, num_peaks, ln):
            # Left side line:
            x = deltas[:ln]
            y = num_peaks[:ln]
            slope, intercept, R, _, _ = stats.linregress(x, y)
            return R, -intercept / slope

        if length > 2:
            # Adjust the first distribution:
            deltas, num_peaks = self.calculate_npeaks_for(max_threshold, steps)

            #  Fit several lines with increasing number of points from the
            #  generated threshold / marker count graph. Stop when the
            #  R-coefficiënt drops below 0.95 (past linear increase from noise)
            #  Then repeat this by increasing the resolution of data points
            #  and continue until the result does not change anymore

            last_threshold = None
            solution = False
            max_iters = 10
            min_iters = 3
            itercount = 0
            if status_dict is not None:
                status_dict["progress"] = 0

            while not solution:
                # Number of points to use for the lin regress:
                ln = 4
                # Maximum number of points to use:
                max_ln = len(deltas)
                # Flag indicating if we can stop searching for the linear part
                stop = False
                while not stop:
                    R, threshold = get_new_threshold(threshold, deltas,
                                                     num_peaks, ln)
                    max_threshold = threshold * 3.2
                    if abs(R) < 0.98 or ln >= max_ln:
                        stop = True
                    else:
                        ln += 1
                itercount += 1  # Increase # of iterations
                if last_threshold:
                    # Check if we have run at least `min_iters`, at most `max_iters`
                    # and have not reached an equilibrium.
                    solution = bool(
                        itercount > min_iters
                        and not (itercount <= max_iters
                                 and last_threshold - threshold >= 0.001))
                    if not solution:
                        deltas, num_peaks = self.calculate_npeaks_for(
                            max_threshold, steps)
                last_threshold = threshold
                if status_dict is not None:
                    status_dict["progress"] = float(itercount / max_iters)

            return (deltas, num_peaks), threshold, max_threshold
        else:
            return ([], []), threshold, max_threshold

    pass  # end of class
Ejemplo n.º 5
0
class Specimen(DataModel, Storable):
    # MODEL INTEL:
    class Meta(DataModel.Meta):
        store_id = "Specimen"

        export_filters = xrd_parsers.get_export_file_filters()
        excl_filters = exc_parsers.get_import_file_filters()

    _data_object = None

    @property
    def data_object(self):
        self._data_object.goniometer = self.goniometer.data_object
        self._data_object.range_theta = self.__get_range_theta()
        self._data_object.selected_range = self.get_exclusion_selector()
        self._data_object.z_list = self.get_z_list()
        try:
            self._data_object.observed_intensity = np.copy(
                self.experimental_pattern.data_y)
        except IndexError:
            self._data_object.observed_intensity = np.array([], dtype=float)
        return self._data_object

    def get_z_list(self):
        return list(self.experimental_pattern.z_data)

    project = property(DataModel.parent.fget, DataModel.parent.fset)

    # PROPERTIES:

    #: The sample name
    sample_name = StringProperty(default="",
                                 text="Sample",
                                 visible=True,
                                 persistent=True,
                                 tabular=True,
                                 signal_name="visuals_changed",
                                 mix_with=(SignalMixin, ))

    #: The sample name
    name = StringProperty(default="",
                          text="Name",
                          visible=True,
                          persistent=True,
                          tabular=True,
                          signal_name="visuals_changed",
                          mix_with=(SignalMixin, ))

    @StringProperty(default="",
                    text="Label",
                    visible=False,
                    persistent=False,
                    tabular=True,
                    mix_with=(ReadOnlyMixin, ))
    def label(self):
        if self.display_stats_in_lbl and (self.project is not None and
                                          self.project.layout_mode == "FULL"):
            label = self.sample_name
            label += "\nRp = %.1f%%" % not_none(self.statistics.Rp, 0.0)
            label += "\nRwp = %.1f%%" % not_none(self.statistics.Rwp, 0.0)
            return label
        else:
            return self.sample_name

    display_calculated = BoolProperty(default=True,
                                      text="Display calculated diffractogram",
                                      visible=True,
                                      persistent=True,
                                      tabular=True,
                                      signal_name="visuals_changed",
                                      mix_with=(SignalMixin, ))

    display_experimental = BoolProperty(
        default=True,
        text="Display experimental diffractogram",
        visible=True,
        persistent=True,
        tabular=True,
        signal_name="visuals_changed",
        mix_with=(SignalMixin, ))

    display_vshift = FloatProperty(default=0.0,
                                   text="Vertical shift of the plot",
                                   visible=True,
                                   persistent=True,
                                   tabular=True,
                                   signal_name="visuals_changed",
                                   widget_type="spin",
                                   mix_with=(SignalMixin, ))

    display_vscale = FloatProperty(default=0.0,
                                   text="Vertical scale of the plot",
                                   visible=True,
                                   persistent=True,
                                   tabular=True,
                                   signal_name="visuals_changed",
                                   widget_type="spin",
                                   mix_with=(SignalMixin, ))

    display_phases = BoolProperty(default=True,
                                  text="Display phases seperately",
                                  visible=True,
                                  persistent=True,
                                  tabular=True,
                                  signal_name="visuals_changed",
                                  mix_with=(SignalMixin, ))

    display_stats_in_lbl = BoolProperty(default=True,
                                        text="Display Rp in label",
                                        visible=True,
                                        persistent=True,
                                        tabular=True,
                                        signal_name="visuals_changed",
                                        mix_with=(SignalMixin, ))

    display_residuals = BoolProperty(default=True,
                                     text="Display residual patterns",
                                     visible=True,
                                     persistent=True,
                                     tabular=True,
                                     signal_name="visuals_changed",
                                     mix_with=(SignalMixin, ))

    display_residual_scale = FloatProperty(default=1.0,
                                           text="Residual pattern scale",
                                           minimum=0.0,
                                           visible=True,
                                           persistent=True,
                                           tabular=True,
                                           signal_name="visuals_changed",
                                           widget_type="spin",
                                           mix_with=(SignalMixin, ))

    display_derivatives = BoolProperty(default=False,
                                       text="Display derivative patterns",
                                       visible=True,
                                       persistent=True,
                                       tabular=True,
                                       signal_name="visuals_changed",
                                       mix_with=(SignalMixin, ))

    #: A :class:`~pyxrd.generic.models.lines.CalculatedLine` instance
    calculated_pattern = LabeledProperty(default=None,
                                         text="Calculated diffractogram",
                                         visible=True,
                                         persistent=True,
                                         tabular=True,
                                         signal_name="data_changed",
                                         widget_type="xy_list_view",
                                         mix_with=(
                                             SignalMixin,
                                             ObserveMixin,
                                         ))

    #: A :class:`~pyxrd.generic.models.lines.ExperimentalLine` instance
    experimental_pattern = LabeledProperty(default=None,
                                           text="Experimental diffractogram",
                                           visible=True,
                                           persistent=True,
                                           tabular=True,
                                           signal_name="data_changed",
                                           widget_type="xy_list_view",
                                           mix_with=(
                                               SignalMixin,
                                               ObserveMixin,
                                           ))

    #: A list of 2-theta ranges to exclude for the calculation of the Rp factor
    exclusion_ranges = LabeledProperty(default=None,
                                       text="Excluded ranges",
                                       visible=True,
                                       persistent=True,
                                       tabular=True,
                                       signal_name="data_changed",
                                       widget_type="xy_list_view",
                                       mix_with=(SignalMixin, ObserveMixin))

    #: A :class:`~pyxrd.goniometer.models.Goniometer` instance
    goniometer = LabeledProperty(default=None,
                                 text="Goniometer",
                                 visible=True,
                                 persistent=True,
                                 tabular=True,
                                 signal_name="data_changed",
                                 mix_with=(
                                     SignalMixin,
                                     ObserveMixin,
                                 ))

    #: A :class:`~pyxrd.specimen.models.Statistics` instance
    statistics = LabeledProperty(
        default=None,
        text="Markers",
        visible=False,
        persistent=False,
        tabular=True,
    )

    #: A list :class:`~pyxrd.specimen.models.Marker` instances
    markers = ListProperty(default=None,
                           text="Markers",
                           data_type=Marker,
                           visible=False,
                           persistent=True,
                           tabular=True,
                           signal_name="visuals_changed",
                           widget_type="object_list_view",
                           mix_with=(SignalMixin, ))

    @property
    def max_display_y(self):
        """
         The maximum intensity or z-value (display y axis) 
         of the current profile (both calculated and observed)
        """
        _max = 0.0
        if self.experimental_pattern is not None:
            _max = max(_max, np.max(self.experimental_pattern.max_display_y))
        if self.calculated_pattern is not None:
            _max = max(_max, np.max(self.calculated_pattern.max_display_y))
        return _max

    # ------------------------------------------------------------
    #      Initialisation and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        """
            Valid keyword arguments for a Specimen are:
                name: the name of the specimen
                sample_name: the sample name of the specimen
                calculated_pattern: the calculated pattern
                experimental_pattern: the experimental pattern
                exclusion_ranges: the exclusion ranges XYListStore
                goniometer: the goniometer used for recording data
                markers: the specimen's markers
                display_vshift: the patterns vertical shift from its default position
                display_vscale: the patterns vertical scale (default is 1.0)
                display_calculated: whether or not to show the calculated pattern
                display_experimental: whether or not to show the experimental pattern
                display_residuals: whether or not to show the residuals
                display_derivatives: whether or not to show the 1st derivative patterns
                display_phases: whether or not to show the separate phase patterns
                display_stats_in_lbl: whether or not to display the Rp values 
                 in the pattern label
        """

        my_kwargs = self.pop_kwargs(
            kwargs, "data_name", "data_sample", "data_sample_length",
            "data_calculated_pattern", "data_experimental_pattern",
            "calc_color", "calc_lw", "inherit_calc_color", "inherit_calc_lw",
            "exp_color", "exp_lw", "inherit_exp_color", "inherit_exp_lw",
            "project_goniometer", "data_markers", "bg_shift", "abs_scale",
            "exp_cap_value", "sample_length", "absorption", "sample_z_dev", *[
                prop.label
                for prop in Specimen.Meta.get_local_persistent_properties()
            ])
        super(Specimen, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        self._data_object = SpecimenData()

        with self.visuals_changed.hold_and_emit():
            with self.data_changed.hold_and_emit():
                self.name = self.get_kwarg(kwargs, "", "name", "data_name")
                sample_name = self.get_kwarg(kwargs, "", "sample_name",
                                             "data_sample")
                if isinstance(sample_name, bytes):
                    sample_name = sample_name.decode("utf-8", "ignore")
                self.sample_name = sample_name

                calc_pattern_old_kwargs = {}
                for kw in ("calc_color", "calc_lw", "inherit_calc_color",
                           "inherit_calc_lw"):
                    if kw in kwargs:
                        calc_pattern_old_kwargs[kw.replace(
                            "calc_", "")] = kwargs.pop(kw)
                self.calculated_pattern = self.parse_init_arg(
                    self.get_kwarg(kwargs, None, "calculated_pattern",
                                   "data_calculated_pattern"),
                    CalculatedLine,
                    child=True,
                    default_is_class=True,
                    label="Calculated Profile",
                    parent=self,
                    **calc_pattern_old_kwargs)

                exp_pattern_old_kwargs = {}
                for kw in ("exp_color", "exp_lw", "inherit_exp_color",
                           "inherit_exp_lw"):
                    if kw in kwargs:
                        exp_pattern_old_kwargs[kw.replace("exp_",
                                                          "")] = kwargs.pop(kw)
                self.experimental_pattern = self.parse_init_arg(
                    self.get_kwarg(kwargs, None, "experimental_pattern",
                                   "data_experimental_pattern"),
                    ExperimentalLine,
                    child=True,
                    default_is_class=True,
                    label="Experimental Profile",
                    parent=self,
                    **exp_pattern_old_kwargs)

                self.exclusion_ranges = PyXRDLine(data=self.get_kwarg(
                    kwargs, None, "exclusion_ranges"),
                                                  parent=self)

                # Extract old kwargs if they are there:
                gonio_kwargs = {}
                sample_length = self.get_kwarg(kwargs, None, "sample_length",
                                               "data_sample_length")
                if sample_length is not None:
                    gonio_kwargs["sample_length"] = float(sample_length)
                absorption = self.get_kwarg(kwargs, None, "absorption")
                if absorption is not None:  # assuming a surface density of at least 20 mg/cm²:
                    gonio_kwargs["absorption"] = float(absorption) / 0.02

                # Initialize goniometer (with optional old kwargs):
                self.goniometer = self.parse_init_arg(self.get_kwarg(
                    kwargs, None, "goniometer", "project_goniometer"),
                                                      Goniometer,
                                                      child=True,
                                                      default_is_class=True,
                                                      parent=self,
                                                      **gonio_kwargs)

                self.markers = self.get_list(kwargs,
                                             None,
                                             "markers",
                                             "data_markers",
                                             parent=self)
                for marker in self.markers:
                    self.observe_model(marker)
                self._specimens_observer = ListObserver(
                    self.on_marker_inserted,
                    self.on_marker_removed,
                    prop_name="markers",
                    model=self)

                self.display_vshift = float(
                    self.get_kwarg(kwargs, 0.0, "display_vshift"))
                self.display_vscale = float(
                    self.get_kwarg(kwargs, 1.0, "display_vscale"))
                self.display_calculated = bool(
                    self.get_kwarg(kwargs, True, "display_calculated"))
                self.display_experimental = bool(
                    self.get_kwarg(kwargs, True, "display_experimental"))
                self.display_residuals = bool(
                    self.get_kwarg(kwargs, True, "display_residuals"))
                self.display_residual_scale = float(
                    self.get_kwarg(kwargs, 1.0, "display_residual_scale"))
                self.display_derivatives = bool(
                    self.get_kwarg(kwargs, False, "display_derivatives"))
                self.display_phases = bool(
                    self.get_kwarg(kwargs, False, "display_phases"))
                self.display_stats_in_lbl = bool(
                    self.get_kwarg(kwargs, True, "display_stats_in_lbl"))

                self.statistics = Statistics(parent=self)

                pass  # end of with
            pass  # end of with
        pass  # end of __init__

    def __str__(self):
        return "<Specimen %s(%s)>" % (self.name, repr(self))

    # ------------------------------------------------------------
    #      Notifications of observable properties
    # ------------------------------------------------------------
    @DataModel.observe("data_changed", signal=True)
    def notify_data_changed(self, model, prop_name, info):
        if model == self.calculated_pattern:
            self.visuals_changed.emit()  # don't propagate this as data_changed
        else:
            self.data_changed.emit()  # propagate signal

    @DataModel.observe("visuals_changed", signal=True)
    def notify_visuals_changed(self, model, prop_name, info):
        self.visuals_changed.emit()  # propagate signal

    def on_marker_removed(self, item):
        with self.visuals_changed.hold_and_emit():
            self.relieve_model(item)
            item.parent = None

    def on_marker_inserted(self, item):
        with self.visuals_changed.hold_and_emit():
            self.observe_model(item)
            item.parent = self

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    @staticmethod
    def from_experimental_data(filename,
                               parent,
                               parser=xrd_parsers._group_parser,
                               load_as_insitu=False):
        """
            Returns a list of new :class:`~.specimen.models.Specimen`'s loaded
            from `filename`, setting their parent to `parent` using the given
            parser. If the load_as_insitu flag is set to true, 
        """
        specimens = list()
        xrdfiles = parser.parse(filename)
        if len(xrdfiles):
            if getattr(xrdfiles[0], "relative_humidity_data",
                       None) is not None:  # we have relative humidity data
                specimen = None

                # Setup list variables:
                x_data = None
                y_datas = []
                rh_datas = []

                for xrdfile in xrdfiles:
                    # Get data we need:
                    name, sample, xy_data, rh_data = (
                        xrdfile.filename, xrdfile.name, xrdfile.data,
                        xrdfile.relative_humidity_data)
                    # Transform into numpy array for column selection
                    xy_data = np.array(xy_data)
                    rh_data = np.array(rh_data)
                    if specimen is None:
                        specimen = Specimen(parent=parent,
                                            name=name,
                                            sample_name=sample)
                        specimen.goniometer.reset_from_file(
                            xrdfile.create_gon_file())
                        # Extract the 2-theta positions once:
                        x_data = np.copy(xy_data[:, 0])

                    # Add a new sub-pattern:
                    y_datas.append(np.copy(xy_data[:, 1]))

                    # Store the average RH for this pattern:
                    rh_datas.append(np.average(rh_data))

                specimen.experimental_pattern.load_data_from_generator(
                    zip(x_data,
                        np.asanyarray(y_datas).transpose()),
                    clear=True)
                specimen.experimental_pattern.y_names = [
                    "%.1f" % f for f in rh_datas
                ]
                specimen.experimental_pattern.z_data = rh_datas
                specimens.append(specimen)
            else:  # regular (might be multi-pattern) file
                for xrdfile in xrdfiles:
                    name, sample, generator = xrdfile.filename, xrdfile.name, xrdfile.data
                    specimen = Specimen(parent=parent,
                                        name=name,
                                        sample_name=sample)
                    # TODO FIXME:
                    specimen.experimental_pattern.load_data_from_generator(
                        generator, clear=True)
                    specimen.goniometer.reset_from_file(
                        xrdfile.create_gon_file())
                    specimens.append(specimen)
        return specimens

    def json_properties(self):
        props = Storable.json_properties(self)
        props["exclusion_ranges"] = self.exclusion_ranges._serialize_data()
        return props

    def get_export_meta_data(self):
        """ Returns a dictionary with common meta-data used in export functions
            for experimental or calculated data """
        return dict(
            sample=self.label + " " + self.sample_name,
            wavelength=self.goniometer.wavelength,
            radius=self.goniometer.radius,
            divergence=self.goniometer.divergence,
            soller1=self.goniometer.soller1,
            soller2=self.goniometer.soller2,
        )

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def clear_markers(self):
        with self.visuals_changed.hold():
            for marker in list(self.markers)[::-1]:
                self.markers.remove(marker)

    def auto_add_peaks(self, tmodel):
        """
        Automagically add peak markers
        
        *tmodel* a :class:`~specimen.models.ThresholdSelector` model
        """
        threshold = tmodel.sel_threshold
        base = 1 if (tmodel.pattern == "exp") else 2
        data_x, data_y = tmodel.get_xy()
        maxtab, mintab = peakdetect(data_y, data_x, 5,
                                    threshold)  # @UnusedVariable

        mpositions = [marker.position for marker in self.markers]

        with self.visuals_changed.hold():
            i = 1
            for x, y in maxtab:  # @UnusedVariable
                if not x in mpositions:
                    nm = self.goniometer.get_nm_from_2t(x) if x != 0 else 0
                    new_marker = Marker(label="%%.%df" %
                                        (3 + min(int(log(nm, 10)), 0)) % nm,
                                        parent=self,
                                        position=x,
                                        base=base)
                    self.markers.append(new_marker)
                i += 1

    def get_exclusion_selector(self):
        """
        Get the numpy selector array for non-excluded data
        
        :rtype: a numpy ndarray
        """
        x = self.__get_range_theta() * 360.0 / pi  # convert to degrees
        selector = np.ones(x.shape, dtype=bool)
        data = np.sort(np.asarray(self.exclusion_ranges.get_xy_data()), axis=0)
        for x0, x1 in zip(*data):
            new_selector = ((x < x0) | (x > x1))
            selector = selector & new_selector
        return selector

    def get_exclusion_xy(self):
        """
        Get an numpy array containing only non-excluded data X and Y data
                
        :rtype: a tuple containing 4 numpy ndarray's: the experimental X and Y
        data and the calculated X and Y data
        """
        ex, ey = self.experimental_pattern.get_xy_data()
        cx, cy = self.calculated_pattern.get_xy_data()
        selector = self.get_exclusion_selector(ex)
        return ex[selector], ey[selector], cx[selector], cy[selector]

    # ------------------------------------------------------------
    #      Draggable mix-in hook:
    # ------------------------------------------------------------
    def on_pattern_dragged(self, delta_y, button=1):
        if button == 1:
            self.display_vshift += delta_y
        elif button == 3:
            self.display_vscale += delta_y
        elif button == 2:
            self.project.display_plot_offset += delta_y
        pass

    def update_visuals(self, phases):
        """
            Update visual representation of phase patterns (if any)
        """
        if phases is not None:
            self.calculated_pattern.y_names = [
                phase.name if phase is not None else "" for phase in phases
            ]
            self.calculated_pattern.phase_colors = [
                phase.display_color if phase is not None else "#FF00FF"
                for phase in phases
            ]

    # ------------------------------------------------------------
    #      Intensity calculations:
    # ------------------------------------------------------------
    def update_pattern(self, total_intensity, phase_intensities, phases):
        """
        Update calculated patterns using the provided total and phase intensities
        """
        if len(phases) == 0:
            self.calculated_pattern.clear()
        else:
            maxZ = len(self.get_z_list())
            new_data = np.zeros(
                (phase_intensities.shape[-1], maxZ + maxZ * len(phases)))
            for z_index in range(maxZ):
                # Set the total intensity for this z_index:
                new_data[:, z_index] = total_intensity[z_index]
                # Calculate phase intensity offsets:
                phase_start_index = maxZ + z_index * len(phases)
                phase_end_index = phase_start_index + len(phases)
                # Set phase intensities for this z_index:
                new_data[:, phase_start_index:
                         phase_end_index] = phase_intensities[:,
                                                              z_index, :].transpose(
                                                              )
                # Store in pattern:
                self.calculated_pattern.set_data(
                    self.__get_range_theta() * 360. / pi, new_data)
            self.update_visuals(phases)
        if settings.GUI_MODE:
            self.statistics.update_statistics(derived=self.display_derivatives)

    def convert_to_fixed(self):
        """
        Converts the experimental data from ADS to fixed slits in-place 
        (disregards the `has_ads` flag in the goniometer, but uses the settings
        otherwise) 
        """
        correction = self.goniometer.get_ADS_to_fixed_correction(
            self.__get_range_theta())
        self.experimental_pattern.apply_correction(correction)

    def convert_to_ads(self):
        """
        Converts the experimental data from fixed slits to ADS in-place 
        (disregards the `has_ads` flag in the goniometer, but uses the settings
        otherwise) 
        """
        correction = 1.0 / self.goniometer.get_ADS_to_fixed_correction(
            self.__get_range_theta())
        self.experimental_pattern.apply_correction(correction)

    def __get_range_theta(self):
        if len(self.experimental_pattern) <= 1:
            return self.goniometer.get_default_theta_range()
        else:
            return np.radians(self.experimental_pattern.data_x * 0.5)

    def __repr__(self):
        return "Specimen(name='%s')" % self.name

    pass  # end of class
Ejemplo n.º 6
0
class Marker(DataModel, Storable, CSVMixin):

    # MODEL INTEL:
    class Meta(DataModel.Meta):
        store_id = "Marker"

    specimen = property(DataModel.parent.fget, DataModel.parent.fset)

    # PROPERTIES:

    #: This marker's label
    label = StringProperty(default="New Marker",
                           text="Label",
                           persistent=True,
                           visible=True,
                           tabular=True,
                           signal_name="visuals_changed",
                           mix_with=(SignalMixin, ))

    #: Flag indicating whether the color of this marker is inherited
    inherit_color = BoolProperty(default=settings.MARKER_INHERIT_COLOR,
                                 text="Inherit color",
                                 persistent=True,
                                 visible=True,
                                 tabular=True,
                                 signal_name="visuals_changed",
                                 mix_with=(SignalMixin, ))
    #: This maker's color:
    color = ColorProperty(default=settings.MARKER_COLOR,
                          text="Color",
                          persistent=True,
                          visible=True,
                          tabular=True,
                          inheritable=True,
                          signal_name="visuals_changed",
                          inherit_flag="inherit_color",
                          inherit_from="specimen.project.display_marker_color",
                          mix_with=(
                              InheritableMixin,
                              SignalMixin,
                          ))

    #: Flag indicating whether the angle of this marker is inherited
    inherit_angle = BoolProperty(default=settings.MARKER_INHERIT_ANGLE,
                                 text="Inherit angle",
                                 persistent=True,
                                 visible=True,
                                 tabular=True,
                                 signal_name="visuals_changed",
                                 mix_with=(SignalMixin, ))
    #: This maker's angle:
    angle = FloatProperty(default=settings.MARKER_ANGLE,
                          text="Angle",
                          widget_type="spin",
                          persistent=True,
                          visible=True,
                          tabular=True,
                          inheritable=True,
                          signal_name="visuals_changed",
                          inherit_flag="inherit_angle",
                          inherit_from="specimen.project.display_marker_angle",
                          mix_with=(
                              InheritableMixin,
                              SignalMixin,
                          ))

    #: Flag indicating whether the top offset of this marker is inherited
    inherit_top_offset = BoolProperty(
        default=settings.MARKER_INHERIT_TOP_OFFSET,
        text="Inherit top offset",
        persistent=True,
        visible=True,
        tabular=True,
        signal_name="visuals_changed",
        mix_with=(SignalMixin, ))
    #: This maker's top offset:
    top_offset = FloatProperty(
        default=settings.MARKER_TOP_OFFSET,
        text="Top offset",
        widget_type="spin",
        persistent=True,
        visible=True,
        tabular=True,
        inheritable=True,
        signal_name="visuals_changed",
        inherit_flag="inherit_top_offset",
        inherit_from="specimen.project.display_marker_top_offset",
        mix_with=(
            InheritableMixin,
            SignalMixin,
        ))

    #: Whether this marker is visible
    visible = BoolProperty(default=settings.MARKER_VISIBLE,
                           text="Visible",
                           persistent=True,
                           visible=True,
                           tabular=True,
                           signal_name="visuals_changed",
                           mix_with=(SignalMixin, ))

    #: The marker's position
    position = FloatProperty(default=settings.MARKER_POSITION,
                             text="Position",
                             widget_type="spin",
                             persistent=True,
                             visible=True,
                             tabular=True,
                             signal_name="visuals_changed",
                             mix_with=(SignalMixin, ))

    #: The marker's x offset
    x_offset = FloatProperty(default=settings.MARKER_X_OFFSET,
                             text="X offset",
                             widget_type="spin",
                             persistent=True,
                             visible=True,
                             tabular=True,
                             signal_name="visuals_changed",
                             mix_with=(SignalMixin, ))

    #: The marker's y offset
    y_offset = FloatProperty(default=settings.MARKER_Y_OFFSET,
                             text="Y offset",
                             widget_type="spin",
                             persistent=True,
                             visible=True,
                             tabular=True,
                             signal_name="visuals_changed",
                             mix_with=(SignalMixin, ))

    #: Flag indicating whether the alignment of this marker is inherited
    inherit_align = BoolProperty(default=settings.MARKER_INHERIT_ALIGN,
                                 text="Inherit align",
                                 persistent=True,
                                 visible=True,
                                 tabular=True,
                                 signal_name="visuals_changed",
                                 mix_with=(SignalMixin, ))
    #: This marker's alignment
    align = StringChoiceProperty(
        default=settings.MARKER_ALIGN,
        text="Align",
        choices=settings.MARKER_ALIGNS,
        persistent=True,
        visible=True,
        tabular=True,
        inheritable=True,
        signal_name="visuals_changed",
        inherit_flag="inherit_align",
        inherit_from="specimen.project.display_marker_align",
        mix_with=(
            InheritableMixin,
            SignalMixin,
        ))

    #: Flag indicating whether the base of this marker is inherited
    inherit_base = BoolProperty(default=settings.MARKER_INHERIT_BASE,
                                text="Inherit base",
                                persistent=True,
                                visible=True,
                                tabular=True,
                                signal_name="visuals_changed",
                                mix_with=(SignalMixin, ))
    #: This marker's base
    base = IntegerChoiceProperty(
        default=settings.MARKER_BASE,
        text="Base",
        choices=settings.MARKER_BASES,
        persistent=True,
        visible=True,
        tabular=True,
        inheritable=True,
        signal_name="visuals_changed",
        inherit_flag="inherit_base",
        inherit_from="specimen.project.display_marker_base",
        mix_with=(
            InheritableMixin,
            SignalMixin,
        ))

    #: Flag indicating whether the top of this marker is inherited
    inherit_top = BoolProperty(default=settings.MARKER_INHERIT_TOP,
                               text="Inherit top",
                               persistent=True,
                               visible=True,
                               tabular=True,
                               signal_name="visuals_changed",
                               mix_with=(SignalMixin, ))
    #: This marker's top
    top = IntegerChoiceProperty(
        default=settings.MARKER_TOP,
        text="Top",
        choices=settings.MARKER_TOPS,
        persistent=True,
        visible=True,
        tabular=True,
        inheritable=True,
        signal_name="visuals_changed",
        inherit_flag="inherit_top",
        inherit_from="specimen.project.display_marker_top",
        mix_with=(
            InheritableMixin,
            SignalMixin,
        ))

    #: Flag indicating whether the line style of this marker is inherited
    inherit_style = BoolProperty(default=settings.MARKER_INHERIT_STYLE,
                                 text="Inherit line style",
                                 persistent=True,
                                 visible=True,
                                 tabular=True,
                                 signal_name="visuals_changed",
                                 mix_with=(SignalMixin, ))
    #: This marker's line style
    style = StringChoiceProperty(
        default=settings.MARKER_STYLE,
        text="Line style",
        choices=settings.MARKER_STYLES,
        persistent=True,
        visible=True,
        tabular=True,
        inheritable=True,
        signal_name="visuals_changed",
        inherit_flag="inherit_style",
        inherit_from="specimen.project.display_marker_style",
        mix_with=(
            InheritableMixin,
            SignalMixin,
        ))

    # ------------------------------------------------------------
    #      Initialisation and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):

        my_kwargs = self.pop_kwargs(
            kwargs, "data_label", "data_visible", "data_position",
            "data_x_offset", "data_y_offset"
            "data_color", "data_base", "data_angle", "data_align", *[
                prop.label
                for prop in type(self).Meta.get_local_persistent_properties()
            ])
        super(Marker, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        self.label = self.get_kwarg(kwargs, "", "label", "data_label")
        self.visible = self.get_kwarg(kwargs, True, "visible", "data_visible")
        self.position = float(
            self.get_kwarg(kwargs, 0.0, "position", "data_position"))
        self.x_offset = float(
            self.get_kwarg(kwargs, 0.0, "x_offset", "data_x_offset"))
        self.y_offset = float(
            self.get_kwarg(kwargs, 0.05, "y_offset", "data_y_offset"))
        self.top_offset = float(self.get_kwarg(kwargs, 0.0, "top_offset"))
        self.color = self.get_kwarg(kwargs, settings.MARKER_COLOR, "color",
                                    "data_color")
        self.base = int(
            self.get_kwarg(kwargs, settings.MARKER_BASE, "base", "data_base"))
        self.angle = float(self.get_kwarg(kwargs, 0.0, "angle", "data_angle"))
        self.align = self.get_kwarg(kwargs, settings.MARKER_ALIGN, "align")
        self.style = self.get_kwarg(kwargs, settings.MARKER_STYLE, "style",
                                    "data_align")

        # if top is not set and style is not "none",
        # assume top to be "Top of plot", otherwise (style is not "none")
        # assume top to be relative to the base point (using top_offset)
        self.top = int(
            self.get_kwarg(kwargs, 0 if self.style == "none" else 1, "top"))

        self.inherit_align = self.get_kwarg(kwargs, True, "inherit_align")
        self.inherit_color = self.get_kwarg(kwargs, True, "inherit_color")
        self.inherit_base = self.get_kwarg(kwargs, True, "inherit_base")
        self.inherit_top = self.get_kwarg(kwargs, True, "inherit_top")
        self.inherit_top_offset = self.get_kwarg(kwargs, True,
                                                 "inherit_top_offset")
        self.inherit_angle = self.get_kwarg(kwargs, True, "inherit_angle")
        self.inherit_style = self.get_kwarg(kwargs, True, "inherit_style")

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def get_nm_position(self):
        if self.parent is not None:
            return self.parent.goniometer.get_nm_from_2t(self.position)
        else:
            return 0.0

    def set_nm_position(self, position):
        if self.parent is not None:
            self.position = self.parent.goniometer.get_2t_from_nm(position)
        else:
            self.position = 0.0

    pass  # end of class
Ejemplo n.º 7
0
class Phase(RefinementGroup, AbstractPhase, metaclass=PyXRDRefinableMeta):

    # MODEL INTEL:
    class Meta(AbstractPhase.Meta):
        store_id = "Phase"
        file_filters = [
            ("Phase file", get_case_insensitive_glob("*.PHS")),
        ]

    _data_object = None

    @property
    def data_object(self):
        self._data_object.type = "Phase"
        self._data_object.valid_probs = (all(self.probabilities.P_valid)
                                         and all(self.probabilities.W_valid))

        if self._data_object.valid_probs:
            self._data_object.sigma_star = self.sigma_star
            self._data_object.CSDS = self.CSDS_distribution.data_object

            self._data_object.G = self.G
            self._data_object.W = self.probabilities.get_distribution_matrix()
            self._data_object.P = self.probabilities.get_probability_matrix()

            self._data_object.components = [None] * len(self.components)
            for i, comp in enumerate(self.components):
                self._data_object.components[i] = comp.data_object
        else:
            self._data_object.sigma_star = None
            self._data_object.CSDS = None
            self._data_object.G = None
            self._data_object.W = None
            self._data_object.P = None
            self._data_object.components = None

        return self._data_object

    project = property(AbstractPhase.parent.fget, AbstractPhase.parent.fset)

    # PROPERTIES:

    #: Flag indicating whether the CSDS distribution is inherited from the
    #: :attr:`based_on` phase or not.
    @BoolProperty(default=False,
                  text="Inh. mean CSDS",
                  visible=True,
                  persistent=True,
                  tabular=True)
    def inherit_CSDS_distribution(self):
        return self._CSDS_distribution.inherited

    @inherit_CSDS_distribution.setter
    def inherit_CSDS_distribution(self, value):
        self._CSDS_distribution.inherited = value

    #: Flag indicating whether to inherit the display color from the
    #: :attr:`based_on` phase or not.
    inherit_display_color = BoolProperty(default=False,
                                         text="Inh. display color",
                                         visible=True,
                                         persistent=True,
                                         tabular=True,
                                         signal_name="visuals_changed",
                                         mix_with=(SignalMixin, ))

    #: Flag indicating whether to inherit the sigma start value from the
    #: :attr:`based_on` phase or not.
    inherit_sigma_star = BoolProperty(default=False,
                                      text="Inh. sigma star",
                                      visible=True,
                                      persistent=True,
                                      tabular=True,
                                      signal_name="data_changed",
                                      mix_with=(SignalMixin, ))

    _based_on_index = None  # temporary property
    _based_on_uuid = None  # temporary property

    #: The :class:`~Phase` instance this phase is based on
    based_on = LabeledProperty(default=None,
                               text="Based on phase",
                               visible=True,
                               persistent=False,
                               tabular=True,
                               signal_name="data_changed",
                               mix_with=(SignalMixin, ObserveChildMixin))

    @based_on.setter
    def based_on(self, value):
        old = type(self).based_on._get(self)
        if value == None or value.get_based_on_root(
        ) == self or value.parent != self.parent:
            value = None
        if value != old:
            type(self).based_on._set(self, value)
            for component in self.components:
                component.linked_with = None

    # INHERITABLE PROPERTIES:

    #: The sigma star orientation factor
    sigma_star = FloatProperty(default=3.0,
                               text="σ* [°]",
                               math_text="$\sigma^*$ [°]",
                               minimum=0.0,
                               maximum=90.0,
                               visible=True,
                               persistent=True,
                               tabular=True,
                               refinable=True,
                               inheritable=True,
                               inherit_flag="inherit_sigma_star",
                               inherit_from="based_on.sigma_star",
                               signal_name="data_changed",
                               mix_with=(SignalMixin, RefinableMixin,
                                         InheritableMixin))

    # A :class:`~pyxrd.phases.models.CSDS` instance
    CSDS_distribution = LabeledProperty(
        default=None,
        text="CSDS Distribution",
        visible=True,
        persistent=True,
        tabular=True,
        refinable=True,
        inheritable=True,
        inherit_flag="inherit_CSDS_distribution",
        inherit_from="based_on.CSDS_distribution",
        signal_name="data_changed",
        mix_with=(SignalMixin, RefinableMixin, InheritableMixin,
                  ObserveChildMixin))

    # A :class:`~pyxrd._probabilities.models._AbstractProbability` subclass instance
    probabilities = LabeledProperty(default=None,
                                    text="Probablities",
                                    visible=True,
                                    persistent=True,
                                    tabular=True,
                                    refinable=True,
                                    signal_name="data_changed",
                                    mix_with=(SignalMixin, RefinableMixin,
                                              ObserveChildMixin))

    @probabilities.setter
    def probabilities(self, value):
        type(self).probabilities._set(self, value)
        if value is not None:
            value.update()

    #: The color this phase's X-ray diffraction pattern should have.
    display_color = StringProperty(fset=AbstractPhase.display_color.fset,
                                   fget=AbstractPhase.display_color.fget,
                                   fdel=AbstractPhase.display_color.fdel,
                                   doc=AbstractPhase.display_color.__doc__,
                                   default="#008600",
                                   text="Display color",
                                   visible=True,
                                   persistent=True,
                                   tabular=True,
                                   widget_type='color',
                                   inheritable=True,
                                   inherit_flag="inherit_display_color",
                                   inherit_from="based_on.display_color",
                                   signal_name="visuals_changed",
                                   mix_with=(SignalMixin, InheritableMixin))

    #: The list of components this phase consists of
    components = ListProperty(default=None,
                              text="Components",
                              visible=True,
                              persistent=True,
                              tabular=True,
                              refinable=True,
                              widget_type="custom",
                              data_type=Component,
                              mix_with=(RefinableMixin, ))

    #: The # of components
    @AbstractPhase.G.getter
    def G(self):
        if self.components is not None:
            return len(self.components)
        else:
            return 0

    #: The # of components
    @AbstractPhase.R.getter
    def R(self):
        if self.probabilities:
            return self.probabilities.R

    # Flag indicating whether or not the links (based_on and linked_with) should
    # be saved as well.
    save_links = True

    # REFINEMENT GROUP IMPLEMENTATION:
    @property
    def refine_title(self):
        return self.name

    @property
    def refine_descriptor_data(self):
        return dict(phase_name=self.refine_title, component_name="*")

    # ------------------------------------------------------------
    #      Initialization and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):

        my_kwargs = self.pop_kwargs(
            kwargs, "data_CSDS_distribution", "data_sigma_star",
            "data_components", "data_G", "G", "data_R", "R",
            "data_probabilities", "based_on_uuid", "based_on_index",
            "inherit_probabilities", *[
                prop.label
                for prop in Phase.Meta.get_local_persistent_properties()
            ])
        super(Phase, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        with self.data_changed.hold():

            CSDS_distribution = self.get_kwarg(kwargs, None,
                                               "CSDS_distribution",
                                               "data_CSDS_distribution")
            self.CSDS_distribution = self.parse_init_arg(CSDS_distribution,
                                                         DritsCSDSDistribution,
                                                         child=True,
                                                         default_is_class=True,
                                                         parent=self)
            self.inherit_CSDS_distribution = self.get_kwarg(
                kwargs, False, "inherit_CSDS_distribution")

            self.display_color = self.get_kwarg(kwargs,
                                                choice(self.line_colors),
                                                "display_color")
            self.inherit_display_color = self.get_kwarg(
                kwargs, False, "inherit_display_color")

            self.sigma_star = self.get_kwarg(kwargs, self.sigma_star,
                                             "sigma_star", "data_sigma_star")
            self.inherit_sigma_star = self.get_kwarg(kwargs, False,
                                                     "inherit_sigma_star")

            self.components = self.get_list(kwargs, [],
                                            "components",
                                            "data_components",
                                            parent=self)

            G = int(self.get_kwarg(kwargs, 1, "G", "data_G"))
            R = int(self.get_kwarg(kwargs, 0, "R", "data_R"))
            if G is not None and G > 0:
                for i in range(len(self.components), G):
                    new_comp = Component(name="Component %d" % (i + 1),
                                         parent=self)
                    self.components.append(new_comp)
                    self.observe_model(new_comp)

            # Observe components
            for component in self.components:
                self.observe_model(component)

            # Connect signals to lists and dicts:
            self._components_observer = ListObserver(
                self.on_component_inserted,
                self.on_component_removed,
                prop_name="components",
                model=self)

            self.probabilities = self.parse_init_arg(
                self.get_kwarg(kwargs, None, "probabilities",
                               "data_probabilities"),
                get_correct_probability_model(R, G),
                default_is_class=True,
                child=True)
            self.probabilities.update()  # force an update
            inherit_probabilities = kwargs.pop("inherit_probabilities", None)
            if inherit_probabilities is not None:
                for prop in self.probabilities.Meta.get_inheritable_properties(
                ):
                    setattr(self.probabilities, prop.inherit_flag,
                            bool(inherit_probabilities))

            self._based_on_uuid = self.get_kwarg(kwargs, None, "based_on_uuid")
            self._based_on_index = self.get_kwarg(kwargs, None,
                                                  "based_on_index")

    def __repr__(self):
        return "Phase(name='%s', based_on=%r)" % (self.name, self.based_on)

    # ------------------------------------------------------------
    #      Notifications of observable properties
    # ------------------------------------------------------------
    def on_component_inserted(self, item):
        # Set parent and observe the new component (visuals changed signals):
        if item.parent != self: item.parent = self
        self.observe_model(item)

    def on_component_removed(self, item):
        with self.data_changed.hold_and_emit():
            # Clear parent & stop observing:
            item.parent = None
            self.relieve_model(item)

    @Observer.observe("data_changed", signal=True)
    def notify_data_changed(self, model, prop_name, info):
        if isinstance(model, Phase) and model == self.based_on:
            with self.data_changed.hold():
                # make sure inherited probabilities are up-to-date
                self.probabilities.update()
                self.data_changed.emit(arg="based_on")
        else:
            self.data_changed.emit()

    @Observer.observe("visuals_changed", signal=True)
    def notify_visuals_changed(self, model, prop_name, info):
        self.visuals_changed.emit()

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    def resolve_json_references(self):
        # Set the based on and linked with variables:
        if hasattr(self, "_based_on_uuid") and self._based_on_uuid is not None:
            self.based_on = type(type(self)).object_pool.get_object(
                self._based_on_uuid)
            del self._based_on_uuid
        elif hasattr(
                self, "_based_on_index"
        ) and self._based_on_index is not None and self._based_on_index != -1:
            warn(
                "The use of object indices is deprecated since version 0.4. Please switch to using object UUIDs.",
                DeprecationWarning)
            self.based_on = self.parent.phases.get_user_from_index(
                self._based_on_index)
            del self._based_on_index
        for component in self.components:
            component.resolve_json_references()
        with self.data_changed.hold():
            # make sure inherited probabilities are up-to-date
            self.probabilities.update()

    def _pre_multi_save(self, phases, ordered_phases):
        ## Override from base class

        if self.based_on != "" and not self.based_on in phases:
            self.save_links = False
        Component.export_atom_types = True
        for component in self.components:
            component.save_links = self.save_links

        # Make sure parent is first in ordered list:
        if self.based_on in phases:
            index = ordered_phases.index(self)
            index2 = ordered_phases.index(self.based_on)
            if index < index2:
                ordered_phases.remove(self.based_on)
                ordered_phases.insert(index, self.based_on)

    def _post_multi_save(self):
        ## Override from base class
        self.save_links = True
        for component in self.components:
            component.save_links = True
        Component.export_atom_types = False

    def json_properties(self):
        retval = super(Phase, self).json_properties()
        if not self.save_links:
            for prop in self.Meta.all_properties:
                if getattr(prop, "inherit_flag", False):
                    retval[prop.inherit_flag] = False
            retval["based_on_uuid"] = ""
        else:
            retval[
                "based_on_uuid"] = self.based_on.uuid if self.based_on else ""
        return retval

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def _update_interference_distributions(self):
        return self.CSDS_distribution.distrib

    def get_based_on_root(self):
        """
            Gets the root object in the based_on chain
        """
        if self.based_on is not None:
            return self.based_on.get_based_on_root()
        else:
            return self

    pass  # end of class
Ejemplo n.º 8
0
class Mixture(DataModel, Storable):
    """
        The base model for optimization and refinement of calculated data
        and experimental data. This is the main model you want to interact with,
        lower-level classes' functionality (mainly 
        :class:`~pyxrd.mixture.models.optimizers.Optimizer` and 
        :class:`~pyxrd.mixture.models.refiner.Refinement`) are integrated into this
        class. 
        
        The Mixture is responsible for managing the phases and specimens lists
        and combination-matrix and for delegating any optimization, calculation
        or refinement calls to the appropriate helper class.
    """
    # MODEL INTEL:
    class Meta(DataModel.Meta):
        store_id = "Mixture"

    _data_object = None
    @property
    def data_object(self):
        self._data_object.parsed = False
        self._data_object.optimized = False
        self._data_object.calculated = False
        self._data_object.specimens = [None] * len(self.specimens)
                
        self._data_object.n = len(self.specimens)
        self._data_object.m = len(self.phases)
               
        self._data_object.scales_mask = np.ones_like(self.scales)
        if self.auto_scales:
            self._data_object.scales_mask = np.ones_like(self.bgshifts)
        else:
            self._data_object.scales_mask = np.zeros_like(self.bgshifts)
            
        if self.auto_bg:
            self._data_object.bgshifts_mask = np.ones_like(self.bgshifts)
        else:
            self._data_object.bgshifts_mask = np.zeros_like(self.bgshifts)
        
        for i, specimen in enumerate(self.specimens):
            if specimen is not None:
                data_object = specimen.data_object
                data_object.phases = [[None] * self._data_object.m for _ in data_object.z_list]
                for z_index in range(len(specimen.get_z_list())):
                    for phase_index in range(self.phase_matrix.shape[1]):
                        data_object.phases[z_index][phase_index] = self.get_phase_data_object(i, z_index, phase_index)
                self._data_object.specimens[i] = data_object
            else:
                self._data_object.specimens[i] = None
        return self._data_object

    def get_phase_data_object(self, specimen_index, z_index, phase_index):
        phase = self.phase_matrix[specimen_index, ...].flatten()[phase_index]
        return phase.data_object if phase is not None else None

    project = property(DataModel.parent.fget, DataModel.parent.fset)

    # SIGNALS:
    #: Signal, emitted when the # of phases or specimens changes
    needs_reset = SignalProperty()
    #: Signal, emitted when the patterns of the specimens need an update
    needs_update = SignalProperty()

    #: The name of this Mixture
    name = StringProperty(
        default="", text="Name",
        visible=True, persistent=True, tabular=True,
        signal_name="visuals_changed",
        mix_with=(SignalMixin,)
    )

    #: Flag, True if the mixture will automatically adjust phase fractions and scales
    auto_run = BoolProperty(
        default=False, text="Auto Run",
        visible=True, persistent=True, tabular=True
    )

    #: Flag, True if the mixture is allowed to also update the background level
    auto_bg = BoolProperty(
        default=False, text="Auto Bg",
        visible=True, persistent=True, tabular=True
    )

    #: Flag, True if the mixture is allowed to also update the background level
    auto_scales = BoolProperty(
        default=True, text="Auto Scales",
        visible=True, persistent=True, tabular=True
    )

    #: The tree of refinable properties
    @LabeledProperty(
        default=None, text="",
        visible=True, persistent=False, tabular=True,
        data_type=object,
    )
    def refinables(self):
        return self.refinement.refinables

    #: An integer describing which method to use for the refinement (see
    #: mixture.models.methods.get_all_refine_methods)
    @IntegerProperty(
        default=0, text="Refinement method index",
        visible=False, persistent=True,
    )
    def refine_method_index(self):
        return self.refinement.refine_method_index

    #: A dict containing the current refinement options
    @LabeledProperty(
        default=None, text="Current refinement method options",
        visible=False, persistent=True, tabular=False,
        data_type=object, store_private="all_refine_options"
    )
    def refine_options(self):
        return  self.refinement.refine_options

    #: A dict containing all refinement options
    @LabeledProperty(
        default=None, text="All refinement methods options",
        visible=False, persistent=False, tabular=False,
        data_type=object
    )
    def all_refine_options(self):
        return self.refinement.all_refine_options

    # Lists and matrices:
    #: A 2D numpy object array containing the combination matrix
    phase_matrix = None
    #: The list of specimen objects
    specimens = None
    #: The list of phase names
    phases = None

    @property
    def scales(self):
        """ A list of floats containing the absolute scales for the calculated patterns """
        return self._data_object.scales
    @scales.setter
    def scales(self, value):
        self._data_object.scales = value

    @property
    def bgshifts(self):
        """ A list of background shifts for the calculated patterns """
        return self._data_object.bgshifts
    @bgshifts.setter
    def bgshifts(self, value):
        self._data_object.bgshifts = value

    @property
    def fractions(self):
        """ A list of phase fractions for this mixture """
        return self._data_object.fractions
    @fractions.setter
    def fractions(self, value):
        self._data_object.fractions = value

    @property
    def fractions_mask(self):
        """ A mask indicating which fractions are to be optimized """
        return self._data_object.fractions_mask
    @fractions_mask.setter
    def fractions_mask(self, value):
        self._data_object.fractions_mask = value

    # ------------------------------------------------------------
    #      Initialization and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        """
            Constructor takes any of its properties as a keyword argument.
            It also support two UUID list keyword arguments:
                - phase_uuids: a list of UUID's for the phases in the mixture
                - specimen_uuids: a list of UUID's for the specimens in the mixture
            These should be *instead* of the phases and specimens keywords.
            
            In addition to the above, the constructor still supports the 
            following deprecated keywords, mapping to a current keyword:
                - phase_indeces: a list of project indices for the phases in the mixture
                - specimen_indeces: a list of project indices for the specimens in the mixture
                
            Any other arguments or keywords are passed to the base class.
        """

        my_kwargs = self.pop_kwargs(kwargs,
            "data_name", "phase_uuids", "phase_indeces", "specimen_uuids",
            "specimen_indeces", "data_phases", "data_scales", "data_bgshifts",
            "data_fractions", "refine_method", "data_refine_method",
            "fractions", "fractions_mask", "bgshifts", "scales", "phases",
            *[prop.label for prop in Mixture.Meta.get_local_persistent_properties()]
        )
        super(Mixture, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        with self.data_changed.hold():

            self._data_object = MixtureData()

            self.name = self.get_kwarg(kwargs, "New Mixture", "name", "data_name")
            self.auto_run = self.get_kwarg(kwargs, False, "auto_run")
            self.auto_bg = self.get_kwarg(kwargs, True, "auto_bg")
            self.auto_scales = self.get_kwarg(kwargs, True, "auto_scales")

            # 2D matrix, rows match specimens, columns match mixture 'phases'; contains the actual phase objects
            phase_uuids = self.get_kwarg(kwargs, None, "phase_uuids")
            phase_indeces = self.get_kwarg(kwargs, None, "phase_indeces")
            if phase_uuids is not None:
                self.phase_matrix = np.array([[type(type(self)).object_pool.get_object(uuid) if uuid else None for uuid in row] for row in phase_uuids], dtype=np.object_)
            elif phase_indeces and self.parent is not None:
                warn("The use of object indices is deprecated since version 0.4. Please switch to using object UUIDs.", DeprecationWarning)
                self.phase_matrix = np.array([[self.parent.phases[index] if index != -1 else None for index in row] for row in phase_indeces], dtype=np.object_)
            else:
                self.phase_matrix = np.empty(shape=(0, 0), dtype=np.object_)

            # list with actual specimens, indexes match with rows in phase_matrix
            specimen_uuids = self.get_kwarg(kwargs, None, "specimen_uuids")
            specimen_indeces = self.get_kwarg(kwargs, None, "specimen_indeces")
            if specimen_uuids:
                self.specimens = [type(type(self)).object_pool.get_object(uuid) if uuid else None for uuid in specimen_uuids]
            elif specimen_indeces and self.parent is not None:
                warn("The use of object indices is deprecated since version 0.4. Please switch to using object UUIDs.", DeprecationWarning)
                self.specimens = [self.parent.specimens[index] if index != -1 else None for index in specimen_indeces]
            else:
                self.specimens = list()

            # list with mixture phase names, indexes match with cols in phase_matrix
            self.phases = self.get_kwarg(kwargs, list(), "phases", "data_phases")

            # list with scale values, indexes match with rows in phase_matrix (= specimens)
            self.scales = np.asarray(self.get_kwarg(kwargs, [1.0] * len(self.specimens), "scales", "data_scales"))
            # list with specimen background shift values, indexes match with rows in phase_matrix (=specimens)
            self.bgshifts = np.asarray(self.get_kwarg(kwargs, [0.0] * len(self.specimens), "bgshifts", "data_bgshifts"))
            # list with phase fractions, indexes match with cols in phase_matrix (=phases)
            self.fractions = np.asarray(self.get_kwarg(kwargs, [0.0] * len(self.phases), "fractions", "data_fractions"))
            # list with phase fractions mask, indexes match with cols in phase_matrix (=phases)
            self.fractions_mask = np.asarray(self.get_kwarg(kwargs, [1] * len(self.phases), "fractions_mask"))

            # sanity check:
            n, m = self.phase_matrix.shape if self.phase_matrix.ndim == 2 else (0, 0)
            if len(self.scales) != n or len(self.specimens) != n or len(self.bgshifts) != n:
                raise IndexError("Shape mismatch: scales (%d), background shifts (%d) or specimens (%d) list lengths do not match with row count (%d) of phase matrix" % (len(self.scales), len(self.specimens), len(self.bgshifts), n))
            if len(self.phases) != m or len(self.fractions) != m:
                raise IndexError("Shape mismatch: fractions (%s) or phases (%d) lists do not match with column count of phase matrix (%d)" % (len(self.fractions), len(self.phases), m))

            self._observe_specimens()
            self._observe_phases()

            self.optimizer = Optimizer(parent=self)
            self.refinement = Refinement(
                refine_method_index=self.get_kwarg(kwargs, 0, "refine_method_index", "refine_method", "data_refine_method"),
                refine_options=self.get_kwarg(kwargs, dict(), "refine_options"),
                parent=self)

            self.update()

            self.observe_model(self)

            pass # end hold data_changed

    # ------------------------------------------------------------
    #      Notifications of observable properties
    # ------------------------------------------------------------
    @DataModel.observe("removed", signal=True)
    def notify_removed(self, model, prop_name, info):
        if model == self:
            self._relieve_phases()
            self._relieve_specimens()

    @DataModel.observe("needs_update", signal=True)
    def notify_needs_update(self, model, prop_name, info):
        with self.data_changed.hold():
            self.update()

    @DataModel.observe("data_changed", signal=True)
    def notify_data_changed(self, model, prop_name, info):
        if not model == self and not (
            info.arg == "based_on" and model.based_on is not None and
            model.based_on in self.phase_matrix):
                self.needs_update.emit()

    @DataModel.observe("visuals_changed", signal=True)
    def notify_visuals_changed(self, model, prop_name, info):
        if isinstance(model, Phase) and \
           not (info.arg == "based_on" and model.based_on is not None and model.based_on in self.phase_matrix):
            for i, specimen in enumerate(self.specimens):
                if specimen is not None:
                    specimen.update_visuals(self.phase_matrix[i, :])

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    def json_properties(self):
        self.refinement.update_refinement_treestore()
        retval = Storable.json_properties(self)

        retval["phase_uuids"] = [[item.uuid if item else "" for item in row] for row in map(list, self.phase_matrix)]
        retval["specimen_uuids"] = [specimen.uuid if specimen else "" for specimen in self.specimens]
        retval["phases"] = self.phases
        retval["fractions"] = self.fractions.tolist()
        retval["fractions_mask"] = self.fractions_mask.tolist()
        retval["bgshifts"] = self.bgshifts.tolist()
        retval["scales"] = self.scales.tolist()

        return retval

    @staticmethod
    def from_json(**kwargs):
        # Remove this deprecated kwarg:
        if "refinables" in kwargs:
            del kwargs["refinables"]
        return Mixture(**kwargs)

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def unset_phase(self, phase):
        """ Clears a phase slot in the phase matrix """
        with self.needs_update.hold_and_emit():
            with self.data_changed.hold():
                shape = self.phase_matrix.shape
                with self._relieve_and_observe_phases():
                    for i in range(shape[0]):
                        for j in range(shape[1]):
                            if self.phase_matrix[i, j] == phase:
                                self.phase_matrix[i, j] = None
                self.refinement.update_refinement_treestore()

    def unset_specimen(self, specimen):
        """ Clears a specimen slot in the specimen list """
        with self.needs_update.hold_and_emit():
            with self.data_changed.hold():
                with self._relieve_and_observe_specimens():
                    for i, spec in enumerate(self.specimens):
                        if spec == specimen:
                            self.specimens[i] = None

    def get_phase(self, specimen_slot, phase_slot):
        """Returns the phase at the given slot positions or None if not set"""
        return self.phase_matrix[specimen_slot, phase_slot]

    def set_phase(self, specimen_slot, phase_slot, phase):
        """Sets the phase at the given slot positions"""
        if self.parent is not None: #no parent = no valid phases
            with self.needs_update.hold_and_emit():
                with self.data_changed.hold():
                    with self._relieve_and_observe_phases():
                        if phase is not None and not phase in self.parent.phases:
                            raise RuntimeError("Cannot add a phase to a Mixture which is not inside the project!")
                        self.phase_matrix[specimen_slot, phase_slot] = phase
                    self.refinement.update_refinement_treestore()

    def get_specimen(self, specimen_slot):
        """Returns the specimen at the given slot position or None if not set"""
        return self.specimens[specimen_slot]

    def set_specimen(self, specimen_slot, specimen):
        """Sets the specimen at the given slot position"""
        if self.parent is not None: #no parent = no valid specimens
            with self.needs_update.hold_and_emit():
                with self.data_changed.hold():
                    with self._relieve_and_observe_specimens():
                        if specimen is not None and not specimen in self.parent.specimens:
                            raise RuntimeError("Cannot add a specimen to a Mixture which is not inside the project!")
                        self.specimens[specimen_slot] = specimen

    @contextmanager
    def _relieve_and_observe_specimens(self):
        self._relieve_specimens()
        yield
        self._observe_specimens()

    def _observe_specimens(self):
        """ Starts observing specimens in the specimens list"""
        for specimen in self.specimens:
            if specimen is not None:
                self.observe_model(specimen)

    def _relieve_specimens(self):
        """ Relieves specimens observer calls """
        for specimen in self.specimens:
            if specimen is not None:
                self.relieve_model(specimen)

    @contextmanager
    def _relieve_and_observe_phases(self):
        self._relieve_phases()
        yield
        self._observe_phases()

    def _observe_phases(self):
        """ Starts observing phases in the phase matrix"""
        for phase in self.phase_matrix.flat:
            if phase is not None:
                self.observe_model(phase)

    def _relieve_phases(self):
        """ Relieves phase observer calls """
        for phase in self.phase_matrix.flat:
            if phase is not None:
                self.relieve_model(phase)

    def add_phase_slot(self, phase_name, fraction):
        """ Adds a new phase column to the phase matrix """
        with self.needs_update.hold_and_emit():
            with self.data_changed.hold():
                self.phases.append(phase_name)
                self.fractions = np.append(self.fractions, fraction)
                self.fractions_mask = np.append(self.fractions_mask, 1)
                n, m = self.phase_matrix.shape if self.phase_matrix.ndim == 2 else (0, 0)
                if self.phase_matrix.size == 0:
                    self.phase_matrix = np.resize(self.phase_matrix.copy(), (n, m + 1))
                    self.phase_matrix[:] = None
                else:
                    self.phase_matrix = np.concatenate([self.phase_matrix.copy(), [[None]] * n ], axis=1)
                    self.phase_matrix[:, m] = None
                self.refinement.update_refinement_treestore()
        return m

    def del_phase_slot(self, phase_slot):
        """ Deletes a phase column using its index """
        with self.needs_update.hold_and_emit():
            with self.data_changed.hold():
                with self._relieve_and_observe_phases():
                    # Remove the corresponding phase name, fraction & references:
                    del self.phases[phase_slot]
                    self.fractions = np.delete(self.fractions, phase_slot)
                    self.fractions_mask = np.delete(self.fractions_mask, phase_slot)
                    self.phase_matrix = np.delete(self.phase_matrix, phase_slot, axis=1)
                # Update our refinement tree store to reflect current state
                self.refinement.update_refinement_treestore()
        # Inform any interested party they need to update their representation
        self.needs_reset.emit()

    def del_phase_slot_by_name(self, phase_name):
        """ Deletes a phase slot using its name """
        self.del_phase_slot(self.phases.index(phase_name))

    def add_specimen_slot(self, specimen, scale, bgs):
        """ Adds a new specimen to the phase matrix (a row) and specimen list """
        with self.needs_update.hold_and_emit():
            with self.data_changed.hold():
                self.specimens.append(None)
                self.scales = np.append(self.scales, scale)
                self.bgshifts = np.append(self.bgshifts, bgs)
                n, m = self.phase_matrix.shape if self.phase_matrix.ndim == 2 else (0, 0)
                if self.phase_matrix.size == 0:
                    self.phase_matrix = np.resize(self.phase_matrix.copy(), (n + 1, m))
                    self.phase_matrix[:] = None
                else:
                    self.phase_matrix = np.concatenate([self.phase_matrix.copy(), [[None] * m] ], axis=0)
                    self.phase_matrix[n, :] = None
                if specimen is not None:
                    self.set_specimen(n, specimen)
        return n

    def del_specimen_slot(self, specimen_slot):
        """ Deletes a specimen slot using its slot index """
        with self.needs_update.hold_and_emit():
            with self.data_changed.hold():
                # Remove the corresponding specimen name, scale, bg-shift & phases:
                with self._relieve_and_observe_specimens():
                    del self.specimens[specimen_slot]
                    self.scales = np.delete(self.scales, specimen_slot)
                    self.bgshifts = np.delete(self.bgshifts, specimen_slot)
                    self.phase_matrix = np.delete(self.phase_matrix, specimen_slot, axis=0)
                # Update our refinement tree store to reflect current state
                self.refinement.update_refinement_treestore()
        # Inform any interested party they need to update their representation
        self.needs_reset.emit()

    def del_specimen_slot_by_object(self, specimen):
        """ Deletes a specimen slot using the actual object """
        try:
            self.del_specimen_slot(self.specimens.index(specimen))
        except ValueError:
            logger.exception("Caught a ValueError when deleting a specimen from  mixture '%s'" % self.name)

    # ------------------------------------------------------------
    #      Refinement stuff:
    # ------------------------------------------------------------
    def set_data_object(self, mixture, calculate=False):
        """
            Sets the fractions, scales and bgshifts of this mixture.
        """
        if mixture is not None:
            with self.needs_update.ignore():
                with self.data_changed.hold_and_emit():
                    self.fractions[:] = list(mixture.fractions)
                    self.scales[:] = list(mixture.scales)
                    self.bgshifts[:] = list(mixture.bgshifts)

                    # reset flag when needed:
                    mixture.calculated = mixture.calculated and not calculate
                    mixture = self.optimizer.calculate(mixture)

                    for i, (specimen_data, specimen) in enumerate(zip(mixture.specimens, self.specimens)):
                        if specimen is not None:
                            with specimen.data_changed.ignore():
                                specimen.update_pattern(
                                    specimen_data.total_intensity,
                                    specimen_data.scaled_phase_intensities,
                                    self.phase_matrix[i, :]
                                )

    def optimize(self):
        """
            Optimize the current solution (fractions, scales, bg shifts & calculate
            phase intensities)
        """
        with self.needs_update.ignore():
            with self.data_changed.hold():
                # no need to re-calculate, is already done by the optimization
                self.set_data_object(self.optimizer.optimize())

    def apply_current_data_object(self):
        """
            Recalculates the intensities using the current fractions, scales
            and bg shifts without optimization
        """
        with self.needs_update.ignore():
            with self.data_changed.hold():
                self.set_data_object(self.data_object, calculate=True)

    # @print_timing
    def update(self):
        """
            Optimizes or re-applies the current mixture 'solution'.
            Effectively re-calculates the entire patterns.
        """
        with self.needs_update.ignore():
            with self.data_changed.hold():
                if self.auto_run:
                    self.optimize()
                else:
                    self.apply_current_data_object()

    # ------------------------------------------------------------
    #      Various other things:
    # ------------------------------------------------------------
    def get_composition_matrix(self):
        """
            Returns a matrix containing the oxide composition for each specimen 
            in this mixture. It uses the COMPOSITION_CONV file for this purpose
            to convert element weights into their oxide weight equivalent.
        """

        # create an atom nr -> (atom name, conversion) mapping
        # this is used to transform most of the elements into their oxides
        atom_conv = OrderedDict()
        with open(settings.DATA_REG.get_file_path("COMPOSITION_CONV"), 'r') as f:
            reader = csv.reader(f)
            next(reader) # skip header
            for row in reader:
                nr, name, fact = row
                atom_conv[int(nr)] = (name, float(fact))

        comps = list()
        for i, row in enumerate(self.phase_matrix):
            comp = dict()
            for j, phase in enumerate(row):
                phase_fract = self.fractions[j]
                for k, component in enumerate(phase.components):
                    comp_fract = phase.probabilities.mW[k] * phase_fract
                    for atom in chain(component.layer_atoms,
                            component.interlayer_atoms):
                        nr = atom.atom_type.atom_nr
                        if nr in atom_conv:
                            wt = atom.pn * atom.atom_type.weight * comp_fract * atom_conv[nr][1]
                            comp[nr] = comp.get(nr, 0.0) + wt
            comps.append(comp)

        final_comps = np.zeros(shape=(len(atom_conv) + 1, len(comps) + 1), dtype='a15')
        final_comps[0, 0] = " "*8
        for j, comp in enumerate(comps):
            fact = 100.0 / sum(comp.values())
            for i, (nr, (oxide_name, conv)) in enumerate(atom_conv.items()):
                wt = comp.get(nr, 0.0) * fact
                # set relevant cells:
                if i == 0:
                    final_comps[i, j + 1] = self.specimens[j].name.ljust(15)[:15]
                if j == 0:
                    final_comps[i + 1, j] = ("%s  " % oxide_name).rjust(8)[:8]
                final_comps[i + 1, j + 1] = ("%.1f" % wt).ljust(15)[:15]

        return final_comps

    pass # end of class
Ejemplo n.º 9
0
class UnitCellProperty(ComponentPropMixin,
                       RefinementValue,
                       DataModel,
                       Storable,
                       metaclass=PyXRDRefinableMeta):
    """
        UnitCellProperty's are an integral part of a component and allow to 
        calculate the dimensions of the unit cell based on compositional
        information such as the iron content.
        This class is not responsible for keeping its value up-to-date.
        With other words, it is the responsibility of the higher-level class
        to call the 'update_value' method on this object whenever it emits a
        'data_changed' signal. The reason for this is to prevent infinite 
        recursion errors. 
    """
    class Meta(DataModel.Meta):
        store_id = "UnitCellProperty"

    component = property(DataModel.parent.fget, DataModel.parent.fset)

    # PROPERTIES:

    #: The UnitCellProperty name
    name = StringProperty(default="",
                          text="Name",
                          visible=False,
                          persistent=False,
                          signal_name="visuals_changed",
                          mix_with=(SignalMixin, ))

    #: Flag indicating if this UnitCellProperty is enabled
    enabled = BoolProperty(default=False,
                           text="Enabled",
                           visible=True,
                           persistent=True,
                           set_action_name="update_value",
                           mix_with=(SetActionMixin, ))

    #: Flag indicating if this UnitCellProperty is inherited
    inherited = BoolProperty(default=False,
                             text="Inherited",
                             visible=False,
                             persistent=False,
                             set_action_name="update_value",
                             mix_with=(SetActionMixin, ))

    #: The value of the UnitCellProperty
    value = FloatProperty(default=0.0,
                          text="Value",
                          visible=True,
                          persistent=True,
                          refinable=True,
                          widget_type='float_entry',
                          set_action_name="update_value",
                          mix_with=(SetActionMixin, RefinableMixin))

    #: The factor of the UnitCellProperty (if enabled and not constant)
    factor = FloatProperty(default=1.0,
                           text="Factor",
                           visible=True,
                           persistent=True,
                           widget_type='float_entry',
                           set_action_name="update_value",
                           mix_with=(SetActionMixin, ))

    #: The constant of the UnitCellProperty (if enabled and not constant)
    constant = FloatProperty(default=0.0,
                             text="Constant",
                             visible=True,
                             persistent=True,
                             widget_type='float_entry',
                             set_action_name="update_value",
                             mix_with=(SetActionMixin, ))

    _temp_prop = None  # temporary, JSON-style prop
    prop = LabeledProperty(default=None,
                           text="Property",
                           visible=True,
                           persistent=True,
                           widget_type='combo',
                           set_action_name="update_value",
                           mix_with=(SetActionMixin, ))

    # REFINEMENT VALUE IMPLEMENTATION:
    @property
    def refine_title(self):
        return self.name

    @property
    def refine_descriptor_data(self):
        return dict(phase_name=self.component.phase.refine_title,
                    component_name=self.component.refine_title)

    @property
    def refine_value(self):
        return self.value

    @refine_value.setter
    def refine_value(self, value):
        if not self.enabled:
            self.value = value

    @property
    def refine_info(self):
        return self.value_ref_info

    @property
    def is_refinable(self):
        return not (self.enabled or self.inherited)

    # ------------------------------------------------------------
    #      Initialisation and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        keys = [
            prop.label for prop in
            UnitCellProperty.Meta.get_local_persistent_properties()
        ]
        keys.extend([
            "data_%s" % prop.label for prop in
            UnitCellProperty.Meta.get_local_persistent_properties()
        ])
        my_kwargs = self.pop_kwargs(kwargs, "name", *keys)
        super(UnitCellProperty, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        with self.data_changed.hold_and_emit():
            self.name = self.get_kwarg(kwargs, self.name, "name", "data_name")
            self.value = self.get_kwarg(kwargs, self.value, "value",
                                        "data_value")
            self.factor = self.get_kwarg(kwargs, self.factor, "factor",
                                         "data_factor")
            self.constant = self.get_kwarg(kwargs, self.constant, "constant",
                                           "data_constant")
            self.enabled = self.get_kwarg(kwargs, self.enabled, "enabled",
                                          "data_enabled")

            self._temp_prop = self.get_kwarg(kwargs, self.prop, "prop",
                                             "data_prop")

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    def json_properties(self):
        retval = Storable.json_properties(self)
        if retval["prop"]:
            # Try to replace objects with their uuid's:
            try:
                retval["prop"] = [
                    getattr(retval["prop"][0], 'uuid', retval["prop"][0]),
                    retval["prop"][1]
                ]
            except:
                logger.exception(
                    "Error when trying to interpret UCP JSON properties")
                pass  # ignore
        return retval

    def resolve_json_references(self):
        if getattr(self, "_temp_prop", None):
            self._temp_prop = list(self._temp_prop)
            if isinstance(self._temp_prop[0], str):
                obj = type(type(self)).object_pool.get_object(
                    self._temp_prop[0])
                if obj:
                    self._temp_prop[0] = obj
                    self.prop = self._temp_prop
                else:
                    self._temp_prop = None
            self.prop = self._temp_prop
            del self._temp_prop

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def create_prop_store(self, extra_props=[]):
        assert (self.component is not None)
        store = Gtk.ListStore(object, str, str)
        # use private properties so we connect to the actual object stores and not the inherited ones
        for atom in self.component._layer_atoms:
            store.append([atom, "pn", atom.name])
        for atom in self.component._interlayer_atoms:
            store.append([atom, "pn", atom.name])
        for prop in extra_props:
            store.append(prop)
        return store

    def get_value_of_prop(self):
        try:
            return getattr(*self.prop)
        except:
            return 0.0

    def update_value(self):
        if self.enabled:
            self._value = float(self.factor * self.get_value_of_prop() +
                                self.constant)
            self.data_changed.emit()

    pass  # end of class
Ejemplo n.º 10
0
class AtomRelation(ComponentPropMixin,
                   RefinementValue,
                   DataModel,
                   Storable,
                   metaclass=PyXRDRefinableMeta):

    # MODEL INTEL:
    class Meta(DataModel.Meta):
        store_id = "AtomRelation"
        file_filters = [
            ("Atom relation", get_case_insensitive_glob("*.atr")),
        ]
        allowed_relations = {}

    component = property(DataModel.parent.fget, DataModel.parent.fset)

    # PROPERTIES:
    #: The name of this AtomRelation
    name = StringProperty(default="",
                          text="Name",
                          visible=True,
                          persistent=True,
                          tabular=True,
                          signal_name="visuals_changed",
                          mix_with=(SignalMixin, ))

    #: The value of this AtomRelation
    value = FloatProperty(default=0.0,
                          text="Value",
                          visible=True,
                          persistent=True,
                          tabular=True,
                          widget_type='float_entry',
                          signal_name="data_changed",
                          refinable=True,
                          mix_with=(SignalMixin, RefinableMixin))

    #: Flag indicating whether this AtomRelation is enabled or not
    enabled = BoolProperty(default=True,
                           text="Enabled",
                           visible=True,
                           persistent=True,
                           tabular=True,
                           signal_name="data_changed",
                           mix_with=(SignalMixin, ))

    #: Is True when this AtomRelation's value is driven by another AtomRelation.
    #: Should never be set directly or things might break!
    driven_by_other = BoolProperty(default=False,
                                   text="Driven by other",
                                   visible=False,
                                   persistent=False,
                                   tabular=True)

    @property
    def applicable(self):
        """
        Is True when this AtomRelation was passed a component of which the atom
        ratios are not set to be inherited from another component.
        """
        return (self.parent is not None
                and not self.parent.inherit_atom_relations)

    # REFINEMENT VALUE IMPLEMENTATION:
    @property
    def refine_title(self):
        return self.name

    @property
    def refine_descriptor_data(self):
        return dict(phase_name=self.component.phase.refine_title,
                    component_name=self.component.refine_title)

    @property
    def refine_value(self):
        return self.value

    @refine_value.setter
    def refine_value(self, value):
        self.value = value

    @property
    def inside_linked_component(self):
        return (self.component.linked_with
                is not None) and self.component.inherit_atom_relations

    @property
    def is_refinable(self):
        return self.enabled and not self.driven_by_other and not self.inside_linked_component

    @property
    def refine_info(self):
        return self.value_ref_info

    # ------------------------------------------------------------
    #      Initialisation and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        """
            Valid keyword arguments for an AtomRelation are:
                name: the name of this AtomRelation
                value: the value for this AtomRelation
                enabled: boolean indicating whether or not this AtomRelation is 
                 enabled
        """
        my_kwargs = self.pop_kwargs(
            kwargs, "data_name", "data_ratio", "ratio", *[
                prop.label for prop in
                AtomRelation.Meta.get_local_persistent_properties()
            ])
        super(AtomRelation, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        self.name = self.get_kwarg(kwargs, "", "name", "data_name")
        self.value = self.get_kwarg(kwargs, 0.0, "value", "ratio",
                                    "data_ratio")
        self.enabled = bool(self.get_kwarg(kwargs, True, "enabled"))

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    def resolve_relations(self):
        raise NotImplementedError(
            "Subclasses should implement the resolve_relations method!")

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def create_prop_store(self, prop=None):
        if self.component is not None:
            store = Gtk.ListStore(object, str, str)
            for atom in self.component._layer_atoms:
                store.append([atom, "pn", atom.name])
            for atom in self.component._interlayer_atoms:
                store.append([atom, "pn", atom.name])
            for relation in self.component._atom_relations:
                tp = relation.Meta.store_id
                if tp in self.Meta.allowed_relations:
                    for prop, name in self.Meta.allowed_relations[tp]:
                        if callable(name):
                            name = name(relation)
                        store.append([relation, prop, name])
            return store

    def iter_references(self):
        raise NotImplementedError(
            "'iter_references' should be implemented by subclasses!")

    def _safe_is_referring(self, value):
        if value is not None and hasattr(value, "is_referring"):
            return value.is_referring([
                self,
            ])
        else:
            return False

    def is_referring(self, references=None):
        """
            Checks whether this AtomRelation is causing circular references.
            Can be used to check this before actually setting references by
            setting the 'references' keyword argument to a list containing the
            new reference value(s).
        """
        if references == None:
            references = []
        # 1. Bluntly check if we're not already somewhere referred to,
        #    if not, add ourselves to the list of references
        if self in references:
            return True
        references.append(self)

        # 2. Loop over our own references, check if they cause a circular
        #    reference, if not add them to the list of references.
        for reference in self.iter_references():
            if reference is not None and hasattr(reference, "is_referring"):
                if reference.is_referring(references):
                    return True
                else:
                    references.append(reference)

        return False

    def _set_driven_flag_for_prop(self, prop=None):
        """Internal method used to safely set the driven_by_other flag on an object.
        Subclasses can override to provide a check on the property set by the driver."""
        self.driven_by_other = True

    def apply_relation(self):
        raise NotImplementedError(
            "Subclasses should implement the apply_relation method!")

    pass  # end of class
Ejemplo n.º 11
0
class Component(RefinementGroup,
                DataModel,
                Storable,
                metaclass=PyXRDRefinableMeta):

    # MODEL INTEL:
    class Meta(DataModel.Meta):
        store_id = "Component"

    _data_object = None

    @property
    def data_object(self):
        weight = 0.0

        self._data_object.layer_atoms = [None] * len(self.layer_atoms)
        for i, atom in enumerate(self.layer_atoms):
            self._data_object.layer_atoms[i] = atom.data_object
            weight += atom.weight

        self._data_object.interlayer_atoms = [None] * len(
            self.interlayer_atoms)
        for i, atom in enumerate(self.interlayer_atoms):
            self._data_object.interlayer_atoms[i] = atom.data_object
            weight += atom.weight

        self._data_object.volume = self.get_volume()
        self._data_object.weight = weight
        self._data_object.d001 = self.d001
        self._data_object.default_c = self.default_c
        self._data_object.delta_c = self.delta_c
        self._data_object.lattice_d = self.lattice_d

        return self._data_object

    phase = property(DataModel.parent.fget, DataModel.parent.fset)

    # SIGNALS:
    atoms_changed = SignalProperty()

    # UNIT CELL DIMENSION SHORTCUTS:
    @property
    def cell_a(self):
        return self._ucp_a.value

    @property
    def cell_b(self):
        return self._ucp_b.value

    @property
    def cell_c(self):
        return self.d001

    # PROPERTIES:

    #: The name of the Component
    name = StringProperty(default="",
                          text="Name",
                          visible=True,
                          persistent=True,
                          tabular=True,
                          signal_name="visuals_changed",
                          mix_with=(SignalMixin, ))

    #: Flag indicating whether to inherit the UCP a from :attr:`~linked_with`
    @BoolProperty(
        default=False,
        text="Inh. cell length a",
        visible=True,
        persistent=True,
        tabular=True,
    )
    def inherit_ucp_a(self):
        return self._ucp_a.inherited

    @inherit_ucp_a.setter
    def inherit_ucp_a(self, value):
        self._ucp_a.inherited = value

    #: Flag indicating whether to inherit the UCP b from :attr:`~linked_with`
    @BoolProperty(
        default=False,
        text="Inh. cell length b",
        visible=True,
        persistent=True,
        tabular=True,
    )
    def inherit_ucp_b(self):
        return self._ucp_b.inherited

    @inherit_ucp_b.setter
    def inherit_ucp_b(self, value):
        self._ucp_b.inherited = value

    #: Flag indicating whether to inherit d001 from :attr:`~linked_with`
    inherit_d001 = BoolProperty(default=False,
                                text="Inh. cell length c",
                                visible=True,
                                persistent=True,
                                tabular=True,
                                signal_name="data_changed",
                                mix_with=(SignalMixin, ))

    #: Flag indicating whether to inherit default_c from :attr:`~linked_with`
    inherit_default_c = BoolProperty(default=False,
                                     text="Inh. default length c",
                                     visible=True,
                                     persistent=True,
                                     tabular=True,
                                     signal_name="data_changed",
                                     mix_with=(SignalMixin, ))

    #: Flag indicating whether to inherit delta_c from :attr:`~linked_with`
    inherit_delta_c = BoolProperty(default=False,
                                   text="Inh. c length dev.",
                                   visible=True,
                                   persistent=True,
                                   tabular=True,
                                   signal_name="data_changed",
                                   mix_with=(SignalMixin, ))

    #: Flag indicating whether to inherit layer_atoms from :attr:`~linked_with`
    inherit_layer_atoms = BoolProperty(default=False,
                                       text="Inh. layer atoms",
                                       visible=True,
                                       persistent=True,
                                       tabular=True,
                                       signal_name="data_changed",
                                       mix_with=(SignalMixin, ))

    #: Flag indicating whether to inherit interlayer_atoms from :attr:`~linked_with`
    inherit_interlayer_atoms = BoolProperty(default=False,
                                            text="Inh. interlayer atoms",
                                            visible=True,
                                            persistent=True,
                                            tabular=True,
                                            signal_name="data_changed",
                                            mix_with=(SignalMixin, ))

    #: Flag indicating whether to inherit atom_relations from :attr:`~linked_with`
    inherit_atom_relations = BoolProperty(default=False,
                                          text="Inh. atom relations",
                                          visible=True,
                                          persistent=True,
                                          tabular=True,
                                          signal_name="data_changed",
                                          mix_with=(SignalMixin, ))

    _linked_with_index = None
    _linked_with_uuid = None

    #: The :class:`~Component` this component is linked with
    linked_with = LabeledProperty(default=None,
                                  text="Linked with",
                                  visible=True,
                                  persistent=True,
                                  signal_name="data_changed",
                                  mix_with=(SignalMixin, ))

    @linked_with.setter
    def linked_with(self, value):
        old = type(self).linked_with._get(self)
        if old != value:
            if old is not None:
                self.relieve_model(old)
            type(self).linked_with._set(self, value)
            if value is not None:
                self.observe_model(value)
            else:
                for prop in self.Meta.get_inheritable_properties():
                    setattr(self, prop.inherit_flag, False)

    #: The silicate lattice's c length
    lattice_d = FloatProperty(default=0.0,
                              text="Lattice c length [nm]",
                              visible=False,
                              persistent=True,
                              signal_name="data_changed")

    ucp_a = LabeledProperty(default=None,
                            text="Cell length a [nm]",
                            visible=True,
                            persistent=True,
                            tabular=True,
                            refinable=True,
                            inheritable=True,
                            inherit_flag="inherit_ucp_a",
                            inherit_from="linked_with.ucp_a",
                            signal_name="data_changed",
                            mix_with=(SignalMixin, InheritableMixin,
                                      ObserveMixin, RefinableMixin))

    ucp_b = LabeledProperty(default=None,
                            text="Cell length b [nm]",
                            visible=True,
                            persistent=True,
                            tabular=True,
                            refinable=True,
                            inheritable=True,
                            inherit_flag="inherit_ucp_b",
                            inherit_from="linked_with.ucp_b",
                            signal_name="data_changed",
                            mix_with=(SignalMixin, InheritableMixin,
                                      ObserveMixin, RefinableMixin))

    d001 = FloatProperty(default=1.0,
                         text="Cell length c [nm]",
                         minimum=0.0,
                         maximum=5.0,
                         visible=True,
                         persistent=True,
                         tabular=True,
                         refinable=True,
                         inheritable=True,
                         inherit_flag="inherit_default_c",
                         inherit_from="linked_with.d001",
                         signal_name="data_changed",
                         mix_with=(SignalMixin, InheritableMixin,
                                   RefinableMixin))

    default_c = FloatProperty(default=1.0,
                              text="Default c length [nm]",
                              minimum=0.0,
                              maximum=5.0,
                              visible=True,
                              persistent=True,
                              tabular=True,
                              inheritable=True,
                              inherit_flag="inherit_default_c",
                              inherit_from="linked_with.default_c",
                              signal_name="data_changed",
                              mix_with=(SignalMixin, InheritableMixin))

    delta_c = FloatProperty(default=0.0,
                            text="C length dev. [nm]",
                            minimum=0.0,
                            maximum=0.05,
                            visible=True,
                            persistent=True,
                            tabular=True,
                            inheritable=True,
                            inherit_flag="inherit_delta_c",
                            inherit_from="linked_with.delta_c",
                            signal_name="data_changed",
                            mix_with=(SignalMixin, InheritableMixin,
                                      RefinableMixin))

    layer_atoms = ListProperty(default=None,
                               text="Layer atoms",
                               visible=True,
                               persistent=True,
                               tabular=True,
                               widget_type="custom",
                               inheritable=True,
                               inherit_flag="inherit_layer_atoms",
                               inherit_from="linked_with.layer_atoms",
                               signal_name="data_changed",
                               data_type=Atom,
                               mix_with=(SignalMixin, InheritableMixin))

    interlayer_atoms = ListProperty(
        default=None,
        text="Interlayer atoms",
        visible=True,
        persistent=True,
        tabular=True,
        widget_type="custom",
        inheritable=True,
        inherit_flag="inherit_interlayer_atoms",
        inherit_from="linked_with.interlayer_atoms",
        signal_name="data_changed",
        data_type=Atom,
        mix_with=(SignalMixin, InheritableMixin))

    atom_relations = ListProperty(default=None,
                                  text="Atom relations",
                                  widget_type="custom",
                                  visible=True,
                                  persistent=True,
                                  tabular=True,
                                  refinable=True,
                                  inheritable=True,
                                  inherit_flag="inherit_atom_relations",
                                  inherit_from="linked_with.atom_relations",
                                  signal_name="data_changed",
                                  data_type=AtomRelation,
                                  mix_with=(SignalMixin, InheritableMixin,
                                            RefinableMixin))

    # Instance flag indicating whether or not linked_with & inherit flags should be saved
    save_links = True
    # Class flag indicating whether or not atom types in the component should be
    # exported using their name rather then their project-uuid.
    export_atom_types = False

    # REFINEMENT GROUP IMPLEMENTATION:
    @property
    def refine_title(self):
        return self.name

    @property
    def refine_descriptor_data(self):
        return dict(phase_name=self.phase.refine_title,
                    component_name=self.refine_title)

    # ------------------------------------------------------------
    #      Initialization and other internals
    # ------------------------------------------------------------
    def __init__(self, **kwargs):
        """
        Valid keyword arguments for a Component are:
        *ucp_a*: unit cell property along a axis
        *ucp_b*: unit cell property along b axis
        *d001*: unit cell length c (aka d001)
        *default_c*: default c-value
        *delta_c*: the variation in basal spacing due to defects
        *layer_atoms*: ObjectListStore of layer Atoms
        *interlayer_atoms*: ObjectListStore of interlayer Atoms
        *atom_relations*: ObjectListStore of AtomRelations
        *inherit_ucp_a*: whether or not to inherit the ucp_a property from
         the linked component (if linked)
        *inherit_ucp_b*: whether or not to inherit the ucp_b property from
         the linked component (if linked)
        *inherit_d001*: whether or not to inherit the d001 property from
         the linked component (if linked)
        *inherit_default_c*: whether or not to inherit the default_c 
         property from the linked component (if linked)
        *inherit_delta_c*: whether or not to inherit the delta_c 
         property from the linked component (if linked)
        *inherit_layer_atoms*: whether or not to inherit the layer_atoms 
         property from the linked component (if linked)
        *inherit_interlayer_atoms*: whether or not to inherit the
         interlayer_atoms property from the linked component (if linked)
        *inherit_atom_relations*: whether or not to inherit the 
         atom_relations property from the linked component (if linked)
        *linked_with_uuid*: the UUID for the component this one is linked
         with
    Deprecated, but still supported:
        *linked_with_index*: the index of the component this one is 
         linked with in the ObjectListStore of the parent based on phase.
        """

        my_kwargs = self.pop_kwargs(
            kwargs, "data_name", "data_layer_atoms", "data_interlayer_atoms",
            "data_atom_relations", "data_atom_ratios", "data_d001",
            "data_default_c", "data_delta_c", "lattice_d", "data_cell_a",
            "data_ucp_a", "data_cell_b", "data_ucp_b", "linked_with_uuid",
            "linked_with_index", "inherit_cell_a", "inherit_cell_b", *[
                prop.label
                for prop in Component.Meta.get_local_persistent_properties()
            ])
        super(Component, self).__init__(**kwargs)
        kwargs = my_kwargs

        # Set up data object
        self._data_object = ComponentData(d001=0.0, delta_c=0.0)

        # Set attributes:
        self.name = self.get_kwarg(kwargs, "", "name", "data_name")

        # Load lists:
        self.layer_atoms = self.get_list(kwargs, [],
                                         "layer_atoms",
                                         "data_layer_atoms",
                                         parent=self)
        self.interlayer_atoms = self.get_list(kwargs, [],
                                              "interlayer_atoms",
                                              "data_interlayer_atoms",
                                              parent=self)
        self.atom_relations = self.get_list(kwargs, [],
                                            "atom_relations",
                                            "data_atom_relations",
                                            parent=self)

        # Add all atom ratios to the AtomRelation list
        for atom_ratio in self.get_list(kwargs, [],
                                        "atom_ratios",
                                        "data_atom_ratios",
                                        parent=self):
            self.atom_relations.append(atom_ratio)

        # Observe the inter-layer atoms, and make sure they get stretched
        for atom in self.interlayer_atoms:
            atom.stretch_values = True
            self.observe_model(atom)

        # Observe the layer atoms
        for atom in self.layer_atoms:
            self.observe_model(atom)

        # Resolve their relations and observe the atom relations
        for relation in self.atom_relations:
            relation.resolve_relations()
            self.observe_model(relation)

        # Connect signals to lists and dicts:
        self._layer_atoms_observer = ListObserver(self._on_layer_atom_inserted,
                                                  self._on_layer_atom_removed,
                                                  prop_name="layer_atoms",
                                                  model=self)
        self._interlayer_atoms_observer = ListObserver(
            self._on_interlayer_atom_inserted,
            self._on_interlayer_atom_removed,
            prop_name="interlayer_atoms",
            model=self)
        self._atom_relations_observer = ListObserver(
            self._on_atom_relation_inserted,
            self._on_atom_relation_removed,
            prop_name="atom_relations",
            model=self)

        # Update lattice values:
        self.d001 = self.get_kwarg(kwargs, self.d001, "d001", "data_d001")
        self._default_c = float(
            self.get_kwarg(kwargs, self.d001, "default_c", "data_default_c"))
        self.delta_c = float(
            self.get_kwarg(kwargs, self.delta_c, "delta_c", "data_delta_c"))
        self.update_lattice_d()

        # Set/Create & observe unit cell properties:
        ucp_a = self.get_kwarg(kwargs, None, "ucp_a", "data_ucp_a",
                               "data_cell_a")
        if isinstance(ucp_a, float):
            ucp_a = UnitCellProperty(name="cell length a",
                                     value=ucp_a,
                                     parent=self)
        ucp_a = self.parse_init_arg(ucp_a,
                                    UnitCellProperty,
                                    child=True,
                                    default_is_class=True,
                                    name="Cell length a [nm]",
                                    parent=self)
        type(self).ucp_a._set(self, ucp_a)
        self.observe_model(ucp_a)

        ucp_b = self.get_kwarg(kwargs, None, "ucp_b", "data_ucp_b",
                               "data_cell_b")
        if isinstance(ucp_b, float):
            ucp_b = UnitCellProperty(name="cell length b",
                                     value=ucp_b,
                                     parent=self)
        ucp_b = self.parse_init_arg(ucp_b,
                                    UnitCellProperty,
                                    child=True,
                                    default_is_class=True,
                                    name="Cell length b [nm]",
                                    parent=self)
        type(self).ucp_b._set(self, ucp_b)
        self.observe_model(ucp_b)

        # Set links:
        self._linked_with_uuid = self.get_kwarg(kwargs, "", "linked_with_uuid")
        self._linked_with_index = self.get_kwarg(kwargs, -1,
                                                 "linked_with_index")

        # Set inherit flags:
        self.inherit_d001 = self.get_kwarg(kwargs, False, "inherit_d001")
        self.inherit_ucp_a = self.get_kwarg(kwargs, False, "inherit_ucp_a",
                                            "inherit_cell_a")
        self.inherit_ucp_b = self.get_kwarg(kwargs, False, "inherit_ucp_b",
                                            "inherit_cell_b")
        self.inherit_default_c = self.get_kwarg(kwargs, False,
                                                "inherit_default_c")
        self.inherit_delta_c = self.get_kwarg(kwargs, False, "inherit_delta_c")
        self.inherit_layer_atoms = self.get_kwarg(kwargs, False,
                                                  "inherit_layer_atoms")
        self.inherit_interlayer_atoms = self.get_kwarg(
            kwargs, False, "inherit_interlayer_atoms")
        self.inherit_atom_relations = self.get_kwarg(kwargs, False,
                                                     "inherit_atom_relations")

    def __repr__(self):
        return "Component(name='%s', linked_with=%r)" % (self.name,
                                                         self.linked_with)

    # ------------------------------------------------------------
    #      Notifications of observable properties
    # ------------------------------------------------------------
    @DataModel.observe("data_changed", signal=True)
    def _on_data_model_changed(self, model, prop_name, info):
        # Check whether the changed model is an AtomRelation or Atom, if so
        # re-apply the atom_relations.
        with self.data_changed.hold():
            if isinstance(model, AtomRelation) or isinstance(model, Atom):
                self._apply_atom_relations()
                self._update_ucp_values()
            if isinstance(model, UnitCellProperty):
                self.data_changed.emit()  # propagate signal

    @DataModel.observe("removed", signal=True)
    def _on_data_model_removed(self, model, prop_name, info):
        # Check whether the removed component is linked with this one, if so
        # clears the link and emits the data_changed signal.
        if model != self and self.linked_with is not None and self.linked_with == model:
            with self.data_changed.hold_and_emit():
                self.linked_with = None

    def _on_layer_atom_inserted(self, atom):
        """Sets the atoms parent and stretch_values property,
        updates the components lattice d-value, and emits a data_changed signal"""
        with self.data_changed.hold_and_emit():
            with self.atoms_changed.hold_and_emit():
                atom.parent = self
                atom.stretch_values = False
                self.observe_model(atom)
                self.update_lattice_d()

    def _on_layer_atom_removed(self, atom):
        """Clears the atoms parent, updates the components lattice d-value, and
        emits a data_changed signal"""
        with self.data_changed.hold_and_emit():
            with self.atoms_changed.hold_and_emit():
                self.relieve_model(atom)
                atom.parent = None
                self.update_lattice_d()

    def _on_interlayer_atom_inserted(self, atom):
        """Sets the atoms parent and stretch_values property, 
        and emits a data_changed signal"""
        with self.data_changed.hold_and_emit():
            with self.atoms_changed.hold_and_emit():
                atom.stretch_values = True
                atom.parent = self

    def _on_interlayer_atom_removed(self, atom):
        """Clears the atoms parent property, 
        and emits a data_changed signal"""
        with self.data_changed.hold_and_emit():
            with self.atoms_changed.hold_and_emit():
                atom.parent = None

    def _on_atom_relation_inserted(self, item):
        item.parent = self
        self.observe_model(item)
        self._apply_atom_relations()

    def _on_atom_relation_removed(self, item):
        self.relieve_model(item)
        item.parent = None
        self._apply_atom_relations()

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    def resolve_json_references(self):
        for atom in type(self).layer_atoms._get(self):
            atom.resolve_json_references()
        for atom in type(self).interlayer_atoms._get(self):
            atom.resolve_json_references()

        type(self).ucp_a._get(self).resolve_json_references()
        type(self).ucp_a._get(self).update_value()
        type(self).ucp_b._get(self).resolve_json_references()
        type(self).ucp_b._get(self).update_value()

        if getattr(self, "_linked_with_uuid", None):
            self.linked_with = type(type(self)).object_pool.get_object(
                self._linked_with_uuid)
            del self._linked_with_uuid
        elif getattr(self, "_linked_with_index",
                     None) and self._linked_with_index != -1:
            warn(
                "The use of object indeces is deprected since version 0.4. Please switch to using object UUIDs.",
                DeprecationWarning)
            self.linked_with = self.parent.based_on.components.get_user_from_index(
                self._linked_with_index)
            del self._linked_with_index

    @classmethod
    def save_components(cls, components, filename):
        """
            Saves multiple components to a single file.
        """
        Component.export_atom_types = True
        for comp in components:
            comp.save_links = False
        with zipfile.ZipFile(filename, 'w', compression=COMPRESSION) as zfile:
            for component in components:
                zfile.writestr(component.uuid, component.dump_object())
        for comp in components:
            comp.save_links = True
        Component.export_atom_types = False

        # After export we change all the UUID's
        # This way, we're sure that we're not going to import objects with
        # duplicate UUID's!
        type(cls).object_pool.change_all_uuids()

    @classmethod
    def load_components(cls, filename, parent=None):
        """
            Returns multiple components loaded from a single file.
        """
        # Before import, we change all the UUID's
        # This way we're sure that we're not going to import objects
        # with duplicate UUID's!
        type(cls).object_pool.change_all_uuids()
        if zipfile.is_zipfile(filename):
            with zipfile.ZipFile(filename, 'r') as zfile:
                for uuid in zfile.namelist():
                    obj = JSONParser.parse(zfile.open(uuid))
                    obj.parent = parent
                    yield obj
        else:
            obj = JSONParser.parse(filename)
            obj.parent = parent
            yield obj

    def json_properties(self):
        if self.phase == None or not self.save_links:
            retval = Storable.json_properties(self)
            for prop in self.Meta.all_properties:
                if getattr(prop, "inherit_flag", False):
                    retval[prop.inherit_flag] = False
        else:
            retval = Storable.json_properties(self)
            retval[
                "linked_with_uuid"] = self.linked_with.uuid if self.linked_with is not None else ""
        return retval

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def get_factors(self, range_stl):
        """
        Get the structure factor for the given range of sin(theta)/lambda values.
        :param range_stl: A 1D numpy ndarray
        """
        return get_factors(range_stl, self.data_object)

    def get_interlayer_stretch_factors(self):
        z_factor = (self.cell_c - self.lattice_d) / (self.default_c -
                                                     self.lattice_d)
        return self.lattice_d, z_factor

    def update_lattice_d(self):
        """
            Updates the lattice_d attribute for this :class:`~.Component`. 
            Should normally not be called from outside the component.
        """
        for atom in self.layer_atoms:
            self.lattice_d = float(max(self.lattice_d, atom.default_z))

    def _apply_atom_relations(self):
        """
        Applies the :class:`~..atom_relations.AtomRelation` objects
        in this component. Should normally not be called from outside the component.
        """
        with self.data_changed.hold_and_emit():
            for relation in self.atom_relations:
                # Clear the 'driven by' flags:
                relation.driven_by_other = False
            for relation in self.atom_relations:
                # Apply the relations, will also take care of flag setting:
                relation.apply_relation()

    def _update_ucp_values(self):
        """
        Updates the :class:`~..unit_cell_prop.UnitCellProperty` objects in this
        component. Should normally not be called from outside the component.
        """
        with self.data_changed.hold():
            for ucp in [self._ucp_a, self._ucp_b]:
                ucp.update_value()

    def get_volume(self):
        """
        Get the volume for this :class:`~.Component`.
        Will always return a value >= 1e-25, to prevent division-by-zero
        errors in calculation code.  
        """
        return max(self.cell_a * self.cell_b * self.cell_c, 1e-25)

    def get_weight(self):
        """
        Get the total atomic weight for this 
        :class:`~.Component`. 
        """
        weight = 0
        for atom in (self.layer_atoms + self.interlayer_atoms):
            weight += atom.weight
        return weight

    # ------------------------------------------------------------
    #      AtomRelation list related
    # ------------------------------------------------------------
    def move_atom_relation_up(self, relation):
        """
        Move the passed :class:`~..atom_relations.AtomRelation`
        up one slot
        """
        index = self.atom_relations.index(relation)
        del self.atom_relations[index]
        self.atom_relations.insert(max(index - 1, 0), relation)

    def move_atom_relation_down(self, relation):
        """
        Move the passed :class:`~..atom_relations.AtomRelation`
        down one slot
        """
        index = self.atom_relations.index(relation)
        del self.atom_relations[index]
        self.atom_relations.insert(min(index + 1, len(self.atom_relations)),
                                   relation)

    pass  # end of class
Ejemplo n.º 12
0
class Project(DataModel, Storable):
    """
    This is the top-level object that servers the purpose of combining the
    different objects (most notably :class:`~.atoms.models.AtomType`'s,
    :class:`~.phases.models.Phase`'s, :class:`~.specimen.models.Specimen`'s and
    :class:`~.mixture.models.Mixture`'s).
    
    It also provides a large number of display-related 'default' properties 
    (e.g. for patterns and their markers, axes etc.). For more details: see the
    property descriptions.
    
    Example usage:
    
    .. code-block:: python
    
        >>> from pyxrd.project.models import Project
        >>> from pyxrd.generic.io.xrd_parsers import XRDParser
        >>> from pyxrd.specimen.models import Specimen
        >>> project = Project(name="New Project", author="Mr. X", layout_mode="FULL", axes_dspacing=True)
        >>> for specimen in Specimen.from_experimental_data("/path/to/xrd_data_file.rd", parent=project):
        ...   project.specimens.append(specimen)
        ...
        
    """

    # MODEL INTEL:
    class Meta(DataModel.Meta):
        store_id = "Project"
        file_filters = [
            ("PyXRD Project files",
             get_case_insensitive_glob("*.pyxrd", "*.zpd")),
        ]
        import_filters = [
            ("Sybilla XML files", get_case_insensitive_glob("*.xml")),
        ]

    # PROPERTIES:

    filename = None

    #: The project name
    name = StringProperty(default="",
                          text="Name",
                          visible=True,
                          persistent=True)

    #: The project data (string)
    date = StringProperty(default="",
                          text="Date",
                          visible=True,
                          persistent=True)

    #: The project description
    description = StringProperty(
        default=None,
        text="Description",
        visible=True,
        persistent=True,
        widget_type="text_view",
    )

    #: The project author
    author = StringProperty(default="",
                            text="Author",
                            visible=True,
                            persistent=True)

    #: Flag indicating whether this project has been changed since it was last saved.
    needs_saving = BoolProperty(default=True, visible=False, persistent=False)

    #: The layout mode this project should be displayed in
    layout_mode = StringChoiceProperty(default=settings.DEFAULT_LAYOUT,
                                       text="Layout mode",
                                       visible=True,
                                       persistent=True,
                                       choices=settings.DEFAULT_LAYOUTS,
                                       mix_with=(SignalMixin, ),
                                       signal_name="visuals_changed")

    #: The manual lower limit for the X-axis
    axes_xmin = FloatProperty(default=settings.AXES_MANUAL_XMIN,
                              text="min. [°2T]",
                              visible=True,
                              persistent=True,
                              minimum=0.0,
                              widget_type="spin",
                              mix_with=(SignalMixin, ),
                              signal_name="visuals_changed")

    #: The manual upper limit for the X-axis
    axes_xmax = FloatProperty(default=settings.AXES_MANUAL_XMAX,
                              text="max. [°2T]",
                              visible=True,
                              persistent=True,
                              minimum=0.0,
                              widget_type="spin",
                              mix_with=(SignalMixin, ),
                              signal_name="visuals_changed")

    #: Whether or not to stretch the X-axis over the entire available display
    axes_xstretch = BoolProperty(default=settings.AXES_XSTRETCH,
                                 text="Stetch x-axis to fit window",
                                 visible=True,
                                 persistent=True,
                                 mix_with=(SignalMixin, ),
                                 signal_name="visuals_changed")

    #: Flag toggling between d-spacing (when True) or 2-Theta axes (when False)
    axes_dspacing = BoolProperty(default=settings.AXES_DSPACING,
                                 text="Show d-spacing in x-axis",
                                 visible=True,
                                 persistent=True,
                                 mix_with=(SignalMixin, ),
                                 signal_name="visuals_changed")

    #: Whether or not the y-axis should be shown
    axes_yvisible = BoolProperty(default=settings.AXES_YVISIBLE,
                                 text="Y-axis visible",
                                 visible=True,
                                 persistent=True,
                                 mix_with=(SignalMixin, ),
                                 signal_name="visuals_changed")

    #: The manual lower limit for the Y-axis (in counts)
    axes_ymin = FloatProperty(default=settings.AXES_MANUAL_YMIN,
                              text="min. [counts]",
                              visible=True,
                              persistent=True,
                              minimum=0.0,
                              widget_type="spin",
                              mix_with=(SignalMixin, ),
                              signal_name="visuals_changed")

    #: The manual upper limit for the Y-axis (in counts)
    axes_ymax = FloatProperty(default=settings.AXES_MANUAL_YMAX,
                              text="max. [counts]",
                              visible=True,
                              persistent=True,
                              minimum=0.0,
                              widget_type="spin",
                              mix_with=(SignalMixin, ),
                              signal_name="visuals_changed")

    #: What type of y-axis to use: raw counts, single or multi-normalized units
    axes_ynormalize = IntegerChoiceProperty(default=settings.AXES_YNORMALIZE,
                                            text="Y scaling",
                                            visible=True,
                                            persistent=True,
                                            choices=settings.AXES_YNORMALIZERS,
                                            mix_with=(SignalMixin, ),
                                            signal_name="visuals_changed")

    #: Whether to use automatic or manual Y limits
    axes_ylimit = IntegerChoiceProperty(default=settings.AXES_YLIMIT,
                                        text="Y limit",
                                        visible=True,
                                        persistent=True,
                                        choices=settings.AXES_YLIMITS,
                                        mix_with=(SignalMixin, ),
                                        signal_name="visuals_changed")

    #: The offset between patterns as a fraction of the maximum intensity
    display_plot_offset = FloatProperty(default=settings.PLOT_OFFSET,
                                        text="Pattern offset",
                                        visible=True,
                                        persistent=True,
                                        minimum=0.0,
                                        widget_type="float_entry",
                                        mix_with=(SignalMixin, ),
                                        signal_name="visuals_changed")

    #: The number of patterns to group ( = having no offset)
    display_group_by = IntegerProperty(default=settings.PATTERN_GROUP_BY,
                                       text="Group patterns by",
                                       visible=True,
                                       persistent=True,
                                       minimum=1,
                                       widget_type="spin",
                                       mix_with=(SignalMixin, ),
                                       signal_name="visuals_changed")

    #: The relative position (from the pattern offset) for pattern labels
    #: as a fraction of the patterns intensity
    display_label_pos = FloatProperty(default=settings.LABEL_POSITION,
                                      text="Default label position",
                                      visible=True,
                                      persistent=True,
                                      widget_type="float_entry",
                                      mix_with=(SignalMixin, ),
                                      signal_name="visuals_changed")

    #: What type of scale to use for X-axis, automatic or manual
    axes_xlimit = IntegerChoiceProperty(default=settings.AXES_XLIMIT,
                                        text="X limit",
                                        visible=True,
                                        persistent=True,
                                        choices=settings.AXES_XLIMITS,
                                        mix_with=(SignalMixin, ),
                                        signal_name="visuals_changed")

    #: The default angle at which marker labels are displayed
    display_marker_angle = FloatProperty(default=settings.MARKER_ANGLE,
                                         text="Angle",
                                         visible=True,
                                         persistent=True,
                                         widget_type="float_entry",
                                         mix_with=(SignalMixin, ),
                                         signal_name="visuals_changed")

    #: The default offset for marker labels
    display_marker_top_offset = FloatProperty(
        default=settings.MARKER_TOP_OFFSET,
        text="Offset from base",
        visible=True,
        persistent=True,
        widget_type="float_entry",
        mix_with=(SignalMixin, ),
        signal_name="visuals_changed")

    #: The default marker label alignment (one of settings.MARKER_ALIGNS)
    display_marker_align = StringChoiceProperty(default=settings.MARKER_ALIGN,
                                                text="Label alignment",
                                                visible=True,
                                                persistent=True,
                                                choices=settings.MARKER_ALIGNS,
                                                mix_with=(SignalMixin, ),
                                                signal_name="visuals_changed")

    #: The default marker label base (one of settings.MARKER_BASES)
    display_marker_base = IntegerChoiceProperty(default=settings.MARKER_BASE,
                                                text="Base connection",
                                                visible=True,
                                                persistent=True,
                                                choices=settings.MARKER_BASES,
                                                mix_with=(SignalMixin, ),
                                                signal_name="visuals_changed")

    #: The default marker label top (one of settings.MARKER_TOPS)
    display_marker_top = IntegerChoiceProperty(default=settings.MARKER_TOP,
                                               text="Top connection",
                                               visible=True,
                                               persistent=True,
                                               choices=settings.MARKER_TOPS,
                                               mix_with=(SignalMixin, ),
                                               signal_name="visuals_changed")

    #: The default marker style (one of settings.MARKER_STYLES)
    display_marker_style = StringChoiceProperty(default=settings.MARKER_STYLE,
                                                text="Line style",
                                                visible=True,
                                                persistent=True,
                                                choices=settings.MARKER_STYLES,
                                                mix_with=(SignalMixin, ),
                                                signal_name="visuals_changed")

    #: The default marker color
    display_marker_color = StringProperty(default=settings.MARKER_COLOR,
                                          text="Color",
                                          visible=True,
                                          persistent=True,
                                          widget_type="color",
                                          mix_with=(SignalMixin, ),
                                          signal_name="visuals_changed")

    #: The default calculated profile color
    display_calc_color = StringProperty(default=settings.CALCULATED_COLOR,
                                        text="Calculated color",
                                        visible=True,
                                        persistent=True,
                                        widget_type="color",
                                        mix_with=(SignalMixin, ),
                                        signal_name="visuals_changed")

    #: The default experimental profile color
    display_exp_color = StringProperty(default=settings.EXPERIMENTAL_COLOR,
                                       text="Experimental color",
                                       visible=True,
                                       persistent=True,
                                       widget_type="color",
                                       mix_with=(SignalMixin, ),
                                       signal_name="visuals_changed")

    #: The default calculated profile line width
    display_calc_lw = IntegerProperty(default=settings.CALCULATED_LINEWIDTH,
                                      text="Calculated line width",
                                      visible=True,
                                      persistent=True,
                                      widget_type="spin",
                                      mix_with=(SignalMixin, ),
                                      signal_name="visuals_changed")

    #: The default experimental profile line width
    display_exp_lw = IntegerProperty(default=settings.EXPERIMENTAL_LINEWIDTH,
                                     text="Experimental line width",
                                     visible=True,
                                     persistent=True,
                                     widget_type="spin",
                                     mix_with=(SignalMixin, ),
                                     signal_name="visuals_changed")

    #: The default calculated profile line style
    display_calc_ls = StringChoiceProperty(
        default=settings.CALCULATED_LINESTYLE,
        text="Calculated line style",
        visible=True,
        persistent=True,
        choices=settings.PATTERN_LINE_STYLES,
        mix_with=(SignalMixin, ),
        signal_name="visuals_changed")

    #: The default experimental profile line style
    display_exp_ls = StringChoiceProperty(
        default=settings.EXPERIMENTAL_LINESTYLE,
        text="Experimental line style",
        visible=True,
        persistent=True,
        choices=settings.PATTERN_LINE_STYLES,
        mix_with=(SignalMixin, ),
        signal_name="visuals_changed")

    #: The default calculated profile line style
    display_calc_marker = StringChoiceProperty(
        default=settings.CALCULATED_MARKER,
        text="Calculated line marker",
        visible=True,
        persistent=True,
        choices=settings.PATTERN_MARKERS,
        mix_with=(SignalMixin, ),
        signal_name="visuals_changed")

    #: The default calculated profile line style
    display_exp_marker = StringChoiceProperty(
        default=settings.EXPERIMENTAL_MARKER,
        text="Experimental line marker",
        visible=True,
        persistent=True,
        choices=settings.PATTERN_MARKERS,
        mix_with=(SignalMixin, ),
        signal_name="visuals_changed")

    #: The list of specimens
    specimens = ListProperty(
        default=[],
        text="Specimens",
        data_type=Specimen,
        visible=True,
        persistent=True,
    )

    #: The list of phases
    phases = ListProperty(default=[],
                          text="Phases",
                          data_type=Phase,
                          visible=False,
                          persistent=True)

    #: The list of atom types
    atom_types = ListProperty(default=[],
                              text="Atom types",
                              data_type=AtomType,
                              visible=False,
                              persistent=True)

    #: The list of Behaviours
    #behaviours = ListProperty(
    #    default=[], text="Behaviours", data_type=InSituBehaviour,
    #    visible=False, persistent=True
    #)

    #: The list of mixtures
    mixtures = ListProperty(default=[],
                            text="Mixture",
                            data_type=Mixture,
                            visible=False,
                            persistent=True)

    # ------------------------------------------------------------
    #      Initialization and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        """
            Constructor takes any of its properties as a keyword argument 
            except for:
                - needs_saving
            
            In addition to the above, the constructor still supports the 
            following deprecated keywords, mapping to a current keyword:
                - goniometer: the project-level goniometer, is passed on to the
                  specimens
                - axes_xscale: deprecated alias for axes_xlimit
                - axes_yscale: deprecated alias for axes_ynormalize
                
            Any other arguments or keywords are passed to the base class.
        """
        my_kwargs = self.pop_kwargs(
            kwargs, "goniometer", "data_goniometer", "data_atom_types",
            "data_phases", "axes_yscale", "axes_xscale", "filename",
            "behaviours", *[
                prop.label
                for prop in Project.Meta.get_local_persistent_properties()
            ])
        super(Project, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        with self.data_changed.hold():
            with self.visuals_changed.hold():

                self.filename = self.get_kwarg(kwargs, self.filename,
                                               "filename")
                self.layout_mode = self.get_kwarg(kwargs, self.layout_mode,
                                                  "layout_mode")

                self.display_marker_align = self.get_kwarg(
                    kwargs, self.display_marker_align, "display_marker_align")
                self.display_marker_color = self.get_kwarg(
                    kwargs, self.display_marker_color, "display_marker_color")
                self.display_marker_base = self.get_kwarg(
                    kwargs, self.display_marker_base, "display_marker_base")
                self.display_marker_top = self.get_kwarg(
                    kwargs, self.display_marker_top, "display_marker_top")
                self.display_marker_top_offset = self.get_kwarg(
                    kwargs, self.display_marker_top_offset,
                    "display_marker_top_offset")
                self.display_marker_angle = self.get_kwarg(
                    kwargs, self.display_marker_angle, "display_marker_angle")
                self.display_marker_style = self.get_kwarg(
                    kwargs, self.display_marker_style, "display_marker_style")

                self.display_calc_color = self.get_kwarg(
                    kwargs, self.display_calc_color, "display_calc_color")
                self.display_exp_color = self.get_kwarg(
                    kwargs, self.display_exp_color, "display_exp_color")
                self.display_calc_lw = self.get_kwarg(kwargs,
                                                      self.display_calc_lw,
                                                      "display_calc_lw")
                self.display_exp_lw = self.get_kwarg(kwargs,
                                                     self.display_exp_lw,
                                                     "display_exp_lw")
                self.display_calc_ls = self.get_kwarg(kwargs,
                                                      self.display_calc_ls,
                                                      "display_calc_ls")
                self.display_exp_ls = self.get_kwarg(kwargs,
                                                     self.display_exp_ls,
                                                     "display_exp_ls")
                self.display_calc_marker = self.get_kwarg(
                    kwargs, self.display_calc_marker, "display_calc_marker")
                self.display_exp_marker = self.get_kwarg(
                    kwargs, self.display_exp_marker, "display_exp_marker")
                self.display_plot_offset = self.get_kwarg(
                    kwargs, self.display_plot_offset, "display_plot_offset")
                self.display_group_by = self.get_kwarg(kwargs,
                                                       self.display_group_by,
                                                       "display_group_by")
                self.display_label_pos = self.get_kwarg(
                    kwargs, self.display_label_pos, "display_label_pos")

                self.axes_xlimit = self.get_kwarg(kwargs, self.axes_xlimit,
                                                  "axes_xlimit", "axes_xscale")
                self.axes_xmin = self.get_kwarg(kwargs, self.axes_xmin,
                                                "axes_xmin")
                self.axes_xmax = self.get_kwarg(kwargs, self.axes_xmax,
                                                "axes_xmax")
                self.axes_xstretch = self.get_kwarg(kwargs, self.axes_xstretch,
                                                    "axes_xstretch")
                self.axes_ylimit = self.get_kwarg(kwargs, self.axes_ylimit,
                                                  "axes_ylimit")
                self.axes_ynormalize = self.get_kwarg(kwargs,
                                                      self.axes_ynormalize,
                                                      "axes_ynormalize",
                                                      "axes_yscale")
                self.axes_yvisible = self.get_kwarg(kwargs, self.axes_yvisible,
                                                    "axes_yvisible")
                self.axes_ymin = self.get_kwarg(kwargs, self.axes_ymin,
                                                "axes_ymin")
                self.axes_ymax = self.get_kwarg(kwargs, self.axes_ymax,
                                                "axes_ymax")

                goniometer = None
                goniometer_kwargs = self.get_kwarg(kwargs, None, "goniometer",
                                                   "data_goniometer")
                if goniometer_kwargs:
                    goniometer = self.parse_init_arg(goniometer_kwargs,
                                                     None,
                                                     child=True)

                # Set up and observe atom types:
                self.atom_types = self.get_list(kwargs, [],
                                                "atom_types",
                                                "data_atom_types",
                                                parent=self)
                self._atom_types_observer = ListObserver(
                    self.on_atom_type_inserted,
                    self.on_atom_type_removed,
                    prop_name="atom_types",
                    model=self)

                # Resolve json references & observe phases
                self.phases = self.get_list(kwargs, [],
                                            "phases",
                                            "data_phases",
                                            parent=self)
                for phase in self.phases:
                    phase.resolve_json_references()
                    self.observe_model(phase)
                self._phases_observer = ListObserver(self.on_phase_inserted,
                                                     self.on_phase_removed,
                                                     prop_name="phases",
                                                     model=self)

                # Set goniometer if required & observe specimens
                self.specimens = self.get_list(kwargs, [],
                                               "specimens",
                                               "data_specimens",
                                               parent=self)
                for specimen in self.specimens:
                    if goniometer: specimen.goniometer = goniometer
                    self.observe_model(specimen)
                self._specimens_observer = ListObserver(
                    self.on_specimen_inserted,
                    self.on_specimen_removed,
                    prop_name="specimens",
                    model=self)

                # Observe behaviours:
                #self.behaviours = self.get_list(kwargs, [], "behaviours", parent=self)
                #for behaviour in self.behaviours:
                #    self.observe_model(behaviour)
                #self._behaviours_observer = ListObserver(
                #    self.on_behaviour_inserted,
                #    self.on_behaviour_removed,
                #    prop_name="behaviours",
                #    model=self
                #)

                # Observe mixtures:
                self.mixtures = self.get_list(kwargs, [],
                                              "mixtures",
                                              "data_mixtures",
                                              parent=self)
                for mixture in self.mixtures:
                    self.observe_model(mixture)
                self._mixtures_observer = ListObserver(
                    self.on_mixture_inserted,
                    self.on_mixture_removed,
                    prop_name="mixtures",
                    model=self)

                self.name = str(
                    self.get_kwarg(kwargs, "Project name", "name",
                                   "data_name"))
                self.date = str(
                    self.get_kwarg(kwargs, time.strftime("%d/%m/%Y"), "date",
                                   "data_date"))
                self.description = str(
                    self.get_kwarg(kwargs, "Project description",
                                   "description", "data_description"))
                self.author = str(
                    self.get_kwarg(kwargs, "Project author", "author",
                                   "data_author"))

                load_default_data = self.get_kwarg(kwargs, True,
                                                   "load_default_data")
                if load_default_data and self.layout_mode != 1 and \
                    len(self.atom_types) == 0:
                    self.load_default_data()

                self.needs_saving = True
            pass  # end with visuals_changed
        pass  # end with data_changed

    def load_default_data(self):
        for atom_type in AtomType.get_from_csv(
                settings.DATA_REG.get_file_path("ATOM_SCAT_FACTORS")):
            self.atom_types.append(atom_type)

    # ------------------------------------------------------------
    #      Notifications of observable properties
    # ------------------------------------------------------------
    def on_phase_inserted(self, item):
        # Set parent on the new phase:
        if item.parent != self: item.parent = self
        item.resolve_json_references()

    def on_phase_removed(self, item):
        with self.data_changed.hold_and_emit():
            # Clear parent:
            item.parent = None
            # Clear links with other phases:
            if getattr(item, "based_on", None) is not None:
                item.based_on = None
            for phase in self.phases:
                if getattr(phase, "based_on", None) == item:
                    phase.based_on = None
            # Remove phase from mixtures:
            for mixture in self.mixtures:
                mixture.unset_phase(item)

    def on_atom_type_inserted(self, item, *data):
        if item.parent != self: item.parent = self
        # We do not observe AtomType's directly, if they change,
        # Atoms containing them will be notified, and that event should bubble
        # up to the project level.

    def on_atom_type_removed(self, item, *data):
        item.parent = None
        # We do not emit a signal for AtomType's, if it was part of
        # an Atom, the Atom will be notified, and the event should bubble
        # up to the project level

    def on_specimen_inserted(self, item):
        # Set parent and observe the new specimen (visuals changed signals):
        if item.parent != self: item.parent = self
        self.observe_model(item)

    def on_specimen_removed(self, item):
        with self.data_changed.hold_and_emit():
            # Clear parent & stop observing:
            item.parent = None
            self.relieve_model(item)
            # Remove specimen from mixtures:
            for mixture in self.mixtures:
                mixture.unset_specimen(item)

    def on_mixture_inserted(self, item):
        # Set parent and observe the new mixture:
        if item.parent != self: item.parent = self
        self.observe_model(item)

    def on_mixture_removed(self, item):
        with self.data_changed.hold_and_emit():
            # Clear parent & stop observing:
            item.parent = None
            self.relieve_model(item)

    def on_behaviour_inserted(self, item):
        # Set parent and observe the new mixture:
        if item.parent != self: item.parent = self
        self.observe_model(item)

    def on_behaviour_removed(self, item):
        with self.data_changed.hold_and_emit():
            # Clear parent & stop observing:
            item.parent = None
            self.relieve_model(item)

    @DataModel.observe("data_changed", signal=True)
    def notify_data_changed(self, model, prop_name, info):
        self.needs_saving = True
        if isinstance(model, Mixture):
            self.data_changed.emit()

    @DataModel.observe("visuals_changed", signal=True)
    def notify_visuals_changed(self, model, prop_name, info):
        self.needs_saving = True
        self.visuals_changed.emit()  # propagate signal

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    @classmethod
    def from_json(type, **kwargs):  # @ReservedAssignment
        project = type(**kwargs)
        project.needs_saving = False  # don't mark this when just loaded
        return project

    def to_json_multi_part(self):
        to_json = self.to_json()
        properties = to_json["properties"]

        for name in ("phases", "specimens", "atom_types",
                     "mixtures"):  #"behaviours"
            yield (name, properties.pop(name))
            properties[name] = "file://%s" % name

        yield ("content", to_json)
        yield ("version", __version__)

    @staticmethod
    def create_from_sybilla_xml(filename, **kwargs):
        from pyxrd.project.importing import create_project_from_sybilla_xml
        return create_project_from_sybilla_xml(filename, **kwargs)

    # ------------------------------------------------------------
    #      Draggable mix-in hook:
    # ------------------------------------------------------------
    def on_label_dragged(self, delta_y, button=1):
        if button == 1:
            self.display_label_pos += delta_y
        pass

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def get_scale_factor(self, specimen=None):
        """
        Get the factor with which to scale raw data and the scaled offset
                
        :rtype: tuple containing the scale factor and the (scaled) offset
        """
        if self.axes_ynormalize == 0 or (self.axes_ynormalize == 1
                                         and specimen is None):
            return (1.0 / (self.get_max_display_y() or 1.0), 1.0)
        elif self.axes_ynormalize == 1:
            return (1.0 / (specimen.get_max_display_y or 1.0), 1.0)
        elif self.axes_ynormalize == 2:
            return (1.0, self.get_max_display_y())
        else:
            raise ValueError(
                "Wrong value for 'axes_ysnormalize' in %s: is `%d`; should be 0, 1 or 2"
                % (self, self.axes_ynormalize))

    def get_max_display_y(self):
        max_display_y = 0
        if self.parent is not None:
            for specimen in self.parent.current_specimens:
                max_display_y = max(specimen.max_display_y, max_display_y)
        return max_display_y

    @contextmanager
    def hold_child_signals(self):
        logger.info("Holding back all project child object signals")
        with self.hold_mixtures_needs_update():
            with self.hold_mixtures_data_changed():
                with self.hold_phases_data_changed():
                    with self.hold_specimens_data_changed():
                        with self.hold_atom_types_data_changed():
                            yield

    @contextmanager
    def hold_mixtures_needs_update(self):
        logger.info("Holding back all 'needs_update' signals from Mixtures")
        with EventContextManager(
                *[mixture.needs_update.hold() for mixture in self.mixtures]):
            yield

    @contextmanager
    def hold_mixtures_data_changed(self):
        logger.info("Holding back all 'data_changed' signals from Mixtures")
        with EventContextManager(
                *[mixture.data_changed.hold() for mixture in self.mixtures]):
            yield

    @contextmanager
    def hold_phases_data_changed(self):
        logger.info("Holding back all 'data_changed' signals from Phases")
        with EventContextManager(
                *[phase.data_changed.hold() for phase in self.phases]):
            yield

    @contextmanager
    def hold_atom_types_data_changed(self):
        logger.info("Holding back all 'data_changed' signals from AtomTypes")
        with EventContextManager(
                *
            [atom_type.data_changed.hold() for atom_type in self.atom_types]):
            yield

    @contextmanager
    def hold_specimens_data_changed(self):
        logger.info("Holding back all 'data_changed' signals from Specimens")
        with EventContextManager(
                *[specimen.data_changed.hold()
                  for specimen in self.specimens]):
            yield

    def update_all_mixtures(self):
        """
        Forces all mixtures in this project to update. If they have auto
        optimization enabled, this will also optimize them. 
        """
        for mixture in self.mixtures:
            with self.data_changed.ignore():
                mixture.update()

    def get_mixtures_by_name(self, mixture_name):
        """
        Convenience method that returns all the mixtures who's name match the
        passed name as a list.
        """
        return [
            mixture for mixture in self.mixtures
            if (mixture.name == mixture_name)
        ]

    # ------------------------------------------------------------
    #      Specimen list related
    # ------------------------------------------------------------
    def move_specimen_up(self, specimen):
        """
        Move the passed :class:`~pyxrd.specimen.models.Specimen` up one slot.
        Will raise and IndexError if the passed specimen is not in this project.
        """
        index = self.specimens.index(specimen)
        self.specimens.insert(min(index + 1, len(self.specimens)),
                              self.specimens.pop(index))

    def move_specimen_down(self, specimen):
        """
        Move the passed :class:`~pyxrd.specimen.models.Specimen` down one slot
        Will raise and IndexError if the passed specimen is not in this project.
        """
        index = self.specimens.index(specimen)
        self.specimens.insert(max(index - 1, 0), self.specimens.pop(index))
        pass

    # ------------------------------------------------------------
    #      Phases list related
    # ------------------------------------------------------------
    def load_phases(self, filename, parser, insert_index=0):
        """
        Loads all :class:`~pyxrd.phase.models.Phase` objects from the file
        'filename'. An optional index can be given where the phases need to be
        inserted at.
        """
        # make sure we have no duplicate UUID's
        insert_index = not_none(insert_index, 0)
        type(Project).object_pool.change_all_uuids()
        for phase in parser.parse(filename):
            phase.parent = self
            self.phases.insert(insert_index, phase)
            insert_index += 1

    # ------------------------------------------------------------
    #      AtomType's list related
    # ------------------------------------------------------------
    def load_atom_types(self, filename, parser):
        """
        Loads all :class:`~pyxrd.atoms.models.AtomType` objects from the
        file specified by *filename*.
        """
        # make sure we have no duplicate UUID's
        type(Project).object_pool.change_all_uuids()
        for atom_type in parser.parse(filename):
            atom_type.parent = self
            self.atom_types.append(atom_type)

    pass  # end of class
Ejemplo n.º 13
0
class _AbstractProbability(RefinementGroup,
                           DataModel,
                           Storable,
                           metaclass=PyXRDRefinableMeta):

    # MODEL INTEL:
    class Meta(DataModel.Meta):
        pass

    phase = property(DataModel.parent.fget, DataModel.parent.fset)

    # PROPERTIES:
    name = StringProperty(default="Probabilities", text="Name")
    W_valid = LabeledProperty(default=None, text="Valid W matrix")
    W_valid_mask = None
    P_valid = LabeledProperty(default=None, text="Valid P matrix")
    P_valid_mask = None

    _R = -1

    @property
    def R(self):
        return self._R

    @property
    def rank(self):
        return self.G**max(self.R, 1)

    _G = 0

    @property
    def G(self):
        return self._G

    _W = None
    _P = None

    @IndexProperty
    def mP(self, indeces):
        r, ind = self._get_Pxy_from_indeces(indeces)
        return self._lP[r][ind]

    @mP.setter
    def mP(self, indeces, value):
        r, ind = self._get_Pxy_from_indeces(indeces)
        self._lP[r][ind] = value

    @IndexProperty
    def mW(self, indeces):
        r, ind = self._get_Wxy_from_indeces(indeces)
        return self._lW[r][ind]

    @mW.setter
    def mW(self, indeces, value):
        r, ind = self._get_Wxy_from_indeces(indeces)
        self._lW[r][ind] = value

    @property
    def _flags(self):
        return [
            1 if getattr(self, prop.inherit_flag, False) else 0
            for prop in self.Meta.all_properties
            if getattr(prop, "inheritable", False)
        ]

    # REFINEMENT GROUP IMPLEMENTATION:
    @property
    def refine_title(self):
        return self.name

    @property
    def refine_descriptor_data(self):
        return dict(phase_name=self.phase.name, component_name="*")

    # ------------------------------------------------------------
    #      Initialization and other internals
    # ------------------------------------------------------------
    def __init__(self, R=-1, *args, **kwargs):
        super(_AbstractProbability, self).__init__(*args, **kwargs)
        self._R = R
        self._create_matrices()

    def _create_matrices(self):
        """
            Creates a list of matrices for different 'levels' of R:
                e.g. when R=3 with g layers there can be 4 different W matrixes:
                    Wi = gxg matrix
                    Wij = g²xg² matrix
                    Wijk = g³xg³ matrix
                    Wijkl = another g³xg³ matrix (= Wijk * Pijkl)
                and 3 different P matrices:
                    Pij = gxg matrix
                    Pijk = g²xg² matrix
                    Pijkl = g³xg³ matrix
        """
        R = max(self.R, 1)
        self._lW = [None] * (R + 1)
        self._lP = [None] * R
        for r in range(R):
            lrank = self.G**(r + 1)
            self._lW[r] = np.zeros(shape=(lrank, lrank), dtype=float)
            self._lP[r] = np.zeros(shape=(lrank, lrank), dtype=float)
        self._lW[-1] = np.zeros(shape=(lrank, lrank), dtype=float)
        self._W = self._lW[-2]
        self._P = self._lP[-1]

        # validity matrices:
        self.W_valid = np.array([False] * (R + 1))
        self.W_valid_mask = np.array([None] * (R + 1))
        self.P_valid = np.array([False] * R)
        self.P_valid_mask = np.array([None] * R)

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def update(self):
        raise NotImplementedError

    def _clamp_set_and_update(self, name, value, minimum=0.0, maximum=1.0):
        clamped = min(max(value, minimum), maximum)
        if getattr(self, name) != clamped:
            setattr(self, name, clamped)
            self.update()

    def solve(self):
        """
            This 'solves' the other W and P matrices using the 'top' P and W
            matrix calculated in the update method.
        """

        for num in range(1, self.R):
            # W matrices:
            for base in product(list(range(self.G)), repeat=num):
                self.mW[base] = 0
                for i in range(self.G):
                    self.mW[base] += self.mW[(i, ) + base]
            # P matrices:
            p_num = num + 1
            for base in product(list(range(self.G)), repeat=p_num):
                W = self.mW[base[:-1]]
                self.mP[base] = self.mW[base] / W if W > 0 else 0.0

        # one extra W matrix:
        self._lW[-1][:] = np.dot(self._W, self._P)

    def validate(self):
        """
            Checks wether the calculated matrices are valid, and stores the
            validation results in 'masks': matrices of the same size, in which
            the values correspond with 1 minus the number of validation rules
            that specific W or P value has failed for.
        """
        def _validate_WW(W, R):
            """Validation rules for the product of a W and P matrix"""
            W_valid_mask = np.ones_like(W)
            rank = self.G**max(R, 1)

            # sum of the cols (W...x's) need to equal W...
            for i in range(rank):
                if abs(np.sum(W[..., i]) - self._W[i, i]) > 1e4:
                    W_valid_mask[..., i] -= 1

            # sum of the entire matrix must equal one:
            if abs(np.sum(W) - 1.0) > 1e4:
                W_valid_mask -= 1

            # values need to be between 0 and 1
            for i in range(rank):
                for j in range(rank):
                    if W[i, j] < 0.0 or W[i, j] > 1.0:
                        W_valid_mask[i, i] -= 1

            # if the sum of the mask values equals the square of the rank,
            # no rules have been broken:
            W_valid = (np.sum(W_valid_mask) == rank**2)

            return W_valid, W_valid_mask

        def _validate_W(W, R):
            """Validation rules for a diagonal W matrix"""
            W_valid_mask = np.ones_like(W)
            rank = self.G**max(R, 1)

            # sum of the diagonal nees to be one
            if abs(np.sum(W) - 1.0) > 1e6:
                for i in range(rank):
                    W_valid_mask[i, i] -= 1

            # values need to be between 0 and 1
            for i in range(rank):
                for j in range(rank):
                    if W[i, j] < 0.0 or W[i, j] > 1.0:
                        W_valid_mask[i, i] -= 1

            # if the sum of the mask values equals the square of the rank,
            # no rules have been broken:
            W_valid = (np.sum(W_valid_mask) == rank**2)

            return W_valid, W_valid_mask

        def _validate_P(P, R):
            P_valid_mask = np.ones_like(P)
            rank = self.G**max(R, 1)

            # sum of the rows need to be one
            for i in range(rank):
                if abs(np.sum(P[i, ...]) - 1.0) > 1e6:
                    P_valid_mask[i, ...] -= 1

            # values need to be between 0 and 1
            for i in range(rank):
                for j in range(rank):
                    if P[i, j] < 0.0 or P[i, j] > 1.0:
                        P_valid_mask[i, j] -= 1

            # if the sum of the mask values equals the square of the rank,
            # no rules have been broken:
            P_valid = (np.sum(P_valid_mask) == rank**2)

            return P_valid, P_valid_mask

        for i in range(max(self.R, 1)):
            self.W_valid[i], self.W_valid_mask[i] = _validate_W(
                self._lW[i], i + 1)
            self.P_valid[i], self.P_valid_mask[i] = _validate_P(
                self._lP[i], i + 1)

        # the extra W matrix validates differently:
        self.W_valid[-1], self.W_valid_mask[-1] = _validate_WW(
            self._lW[-1], self.R)

    def _get_Pxy_from_indeces(self, indeces):
        if not hasattr(indeces, "__iter__"):
            indeces = [indeces]
        l = len(indeces)
        assert (
            l > 1
        ), "Two or more indeces needed to acces P elements, not %s" % indeces
        assert (l <= max(self.R, 1) +
                1), "Too many indeces for an R%d model: %s" % (self.R, indeces)
        R = max(l - 1, 1)
        x, y = 0, 0
        for i in range(1, R + 1):
            f = self.G**(R - i)
            x += indeces[i - 1] * f
            y += indeces[i] * f
        return (l - 2), (x, y)

    def _get_Wxy_from_indeces(self, indeces):
        if not hasattr(indeces, "__iter__"):
            indeces = [indeces]
        l = len(indeces)
        assert (l > 0), "One or more indeces needed to acces W elements"
        assert (l <= max(self.R, 1) +
                1), "Too many indeces for an R%d model: %s" % (self.R, indeces)
        if l == (max(self.R, 1) + 1):
            R = max(l - 1, 1)
            x, y = 0, 0
            for i in range(1, R + 1):
                f = self.G**(R - i)
                x += indeces[i - 1] * f
                y += indeces[i] * f
            return (l - 1), (x, y)
        else:
            R = max(l, 1)
            x = 0
            for i in range(R):
                x += indeces[i] * self.G**(R - (i + 1))
            return (l - 1), (x, x)

    def get_all_matrices(self):
        return self._lW, self._lP

    def get_distribution_matrix(self):
        return self._W

    def get_distribution_array(self):
        return np.diag(self._W)

    def get_probability_matrix(self):
        return self._P

    _stashed_lP = None
    _stashed_lW = None
    _stashed_flags = None

    def _stash_matrices(self):
        """ Stashes the matrices for an update """
        self._stashed_lW = deepcopy(np.asanyarray(self._lW))
        self._stashed_lP = deepcopy(np.asanyarray(self._lP))
        self._stashed_flags = deepcopy(np.asarray(self._flags))

    def _compare_stashed_matrices(self):
        """ Unstashed matrices and compares with current values, if identical returns True """
        if self._stashed_lP is not None and self._stashed_lW is not None:
            result = np.array_equal(self._stashed_lW, np.asanyarray(self._lW))
            result = result and np.array_equal(self._stashed_lP,
                                               np.asanyarray(self._lP))
            result = result and np.array_equal(self._stashed_flags,
                                               np.asanyarray(self._flags))
            self._stashed_lW = None
            self._stashed_lP = None
            return result
        else:
            return False

    @contextmanager
    def monitor_changes(self):
        with self.data_changed.hold():
            self._stash_matrices()
            yield
            if not self._compare_stashed_matrices():
                self.data_changed.emit()

    pass  # end of class