Esempio n. 1
0
class CalculatedLine(PyXRDLine):

    # MODEL INTEL:
    class Meta(PyXRDLine.Meta):
        store_id = "CalculatedLine"
        inherit_format = "display_calc_%s"

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

    # PROPERTIES:

    phase_colors = ListProperty(
        default=[],
        test="Phase colors",
        mix_with=(SignalMixin, ),
        signal_name="visuals_changed",
    )

    #: The line color
    color = modify(PyXRDLine.color,
                   default=settings.CALCULATED_COLOR,
                   inherit_from="parent.parent.display_calc_color")

    #: The linewidth in points
    lw = modify(PyXRDLine.lw,
                default=settings.CALCULATED_LINEWIDTH,
                inherit_from="parent.parent.display_calc_lw")

    #: A short string describing the (matplotlib) linestyle
    ls = modify(PyXRDLine.ls,
                default=settings.CALCULATED_LINESTYLE,
                inherit_from="parent.parent.display_calc_ls")

    #: A short string describing the (matplotlib) marker
    marker = modify(PyXRDLine.marker,
                    default=settings.CALCULATED_MARKER,
                    inherit_from="parent.parent.display_calc_marker")

    pass  # end of class
Esempio n. 2
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
Esempio n. 3
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
Esempio n. 4
0
class MineralScorer(DataModel):

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

    matches_changed = SignalProperty()

    matches = ListProperty(default=None,
                           text="Matches",
                           visible=True,
                           persistent=False,
                           mix_with=(ReadOnlyMixin, ))

    @ListProperty(default=None,
                  text="Minerals",
                  visible=True,
                  persistent=False,
                  mix_with=(ReadOnlyMixin, ))
    def minerals(self):
        # Load them when accessed for the first time:
        _minerals = type(self).minerals._get(self)
        if _minerals == None:
            _minerals = list()
            with unicode_open(
                    settings.DATA_REG.get_file_path("MINERALS")) as f:
                mineral = ""
                abbreviation = ""
                position_flag = True
                peaks = []
                for line in f:
                    line = line.replace('\n', '')
                    try:
                        number = float(line)
                        if position_flag:
                            position = number
                        else:
                            intensity = number
                            peaks.append((position, intensity))
                        position_flag = not position_flag
                    except ValueError:
                        if mineral != "":
                            _minerals.append((mineral, abbreviation, peaks))
                        position_flag = True
                        if len(line) > 25:
                            mineral = line[:24].strip()
                        if len(line) > 49:
                            abbreviation = line[49:].strip()
                        peaks = []
            sorted(_minerals, key=lambda mineral: mineral[0])
        type(self).minerals._set(self, _minerals)
        return _minerals

    # ------------------------------------------------------------
    #      Initialisation and other internals
    # ------------------------------------------------------------
    def __init__(self, marker_peaks=[], *args, **kwargs):
        super(MineralScorer, self).__init__(*args, **kwargs)
        self._matches = []

        self.marker_peaks = marker_peaks  # position, intensity

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def auto_match(self):
        self._matches = score_minerals(self.marker_peaks, self.minerals)
        self.matches_changed.emit()

    def del_match(self, index):
        if self.matches:
            del self.matches[index]
            self.matches_changed.emit()

    def add_match(self, name, abbreviation, peaks):
        matches = score_minerals(self.marker_peaks,
                                 [(name, abbreviation, peaks)])
        if len(matches):
            name, abbreviation, peaks, matches, score = matches[0]
        else:
            matches, score = [], 0.
        self.matches.append([name, abbreviation, peaks, matches, score])
        sorted(self._matches, key=lambda match: match[-1], reverse=True)
        self.matches_changed.emit()

    pass  # end of class
Esempio n. 5
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
Esempio n. 6
0
class AtomContents(AtomRelation):

    # MODEL INTEL:
    class Meta(AtomRelation.Meta):
        store_id = "AtomContents"
        allowed_relations = {
            "AtomRatio": [
                ("__internal_sum__", lambda o: "%s: SUM" % o.name),
                ("value", lambda o: "%s: RATIO" % o.name),
            ],
        }

    # SIGNALS:

    # PROPERTIES:
    atom_contents = ListProperty(default=None,
                                 text="Atom contents",
                                 visible=True,
                                 persistent=True,
                                 tabular=True,
                                 data_type=AtomContentObject,
                                 mix_with=(ReadOnlyMixin, ))

    # ------------------------------------------------------------
    #      Initialisation and other internals
    # ------------------------------------------------------------
    def __init__(self, *args, **kwargs):
        """
            Valid keyword arguments for an AtomContents are:
                atom_contents: a list of tuples containing the atom content 
                 object uuids, property names and default amounts 
        """
        my_kwargs = self.pop_kwargs(
            kwargs, *[
                prop.label for prop in
                AtomContents.Meta.get_local_persistent_properties()
            ])
        super(AtomContents, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        # Load atom contents:
        atom_contents = []
        for uuid, prop, amount in self.get_kwarg(kwargs, [], "atom_contents"):
            # uuid's are resolved when resolve_relations is called
            atom_contents.append(AtomContentObject(uuid, prop, amount))
        type(self).atom_contents._set(self, atom_contents)

        def on_change(*args):
            if self.enabled:  # no need for updates otherwise
                self.data_changed.emit()

        self._atom_contents_observer = ListObserver(on_change,
                                                    on_change,
                                                    prop_name="atom_contents",
                                                    model=self)

    # ------------------------------------------------------------
    #      Input/Output stuff
    # ------------------------------------------------------------
    def json_properties(self):
        retval = Storable.json_properties(self)
        retval["atom_contents"] = list([[
            atom_contents.atom.uuid if atom_contents.atom else None,
            atom_contents.prop, atom_contents.amount
        ] for atom_contents in retval["atom_contents"]])
        return retval

    def resolve_relations(self):
        # Disable event dispatching to prevent infinite loops
        enabled = self.enabled
        self.enabled = False
        # Change rows with string references to objects (uuid's)
        for atom_content in self.atom_contents:
            if isinstance(atom_content.atom, str):
                atom_content.atom = type(type(self)).object_pool.get_object(
                    atom_content.atom)
        # Set the flag to its original value
        self.enabled = enabled

    # ------------------------------------------------------------
    #      Methods & Functions
    # ------------------------------------------------------------
    def apply_relation(self):
        if self.enabled and self.applicable:
            for atom_content in self.atom_contents:
                atom_content.update_atom(self.value)

    def set_atom_content_values(self, path, new_atom, new_prop):
        """    
            Convenience function that first checks if the new atom value will
            not cause a circular reference before actually setting it.
        """
        with self.data_changed.hold():
            atom_content = self.atom_contents[int(path[0])]
            if atom_content.atom != new_atom:
                old_atom = atom_content.atom
                atom_content.atom = None  # clear...
                if not self._safe_is_referring(new_atom):
                    atom_content.atom = new_atom
                else:
                    atom_content.atom = old_atom
            else:
                atom_content.atom = None
            atom_content.prop = new_prop

    def iter_references(self):
        for atom_content in self.atom_contents:
            yield atom_content.atom

    pass  # end of class
Esempio n. 7
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
Esempio n. 8
0
class Refinement(ChildModel):
    """
        A simple model that plugs onto the Mixture model. It provides
        the functionality related to refinement of parameters.
    """

    # MODEL INTEL:
    class Meta(ChildModel.Meta):
        store_id = "Refinement"

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

    #: Flag, True if after refinement plots should be generated of the parameter space
    make_psp_plots = BoolProperty(default=False,
                                  text="Make parameter space plots",
                                  tabular=False,
                                  visible=True,
                                  persistent=True)

    #: TreeNode containing the refinable properties
    refinables = ListProperty(default=None,
                              text="Refinables",
                              tabular=True,
                              persistent=False,
                              visible=True,
                              data_type=RefinableWrapper,
                              cast_to=None,
                              widget_type="object_tree_view")

    #: A dict containing an instance of each refinement method
    refine_methods = None

    #: An integer describing which method to use for the refinement
    refine_method_index = IntegerChoiceProperty(
        default=0,
        text="Refinement method index",
        tabular=True,
        persistent=True,
        visible=True,
        choices={
            key: method.name
            for key, method in RefineMethodManager.get_all_methods().items()
        })

    #: A dict containing the current refinement options
    @LabeledProperty(default=None,
                     text="Refine options",
                     persistent=False,
                     visible=False,
                     mix_with=(ReadOnlyMixin, ))
    def refine_options(self):
        return self.get_refinement_method().get_options()

    #: A dict containing all refinement options
    @property
    def all_refine_options(self):
        return {
            method.index: method.get_options()
            for method in list(self.refine_methods.values())
        }

    def __init__(self, *args, **kwargs):
        my_kwargs = self.pop_kwargs(kwargs, "refine_method_index",
                                    "refine_method", "refine_options")
        super(Refinement, self).__init__(*args, **kwargs)
        kwargs = my_kwargs

        # Setup the refinables treestore
        self.refinables = TreeNode()
        self.update_refinement_treestore()

        # Setup the refine methods
        try:
            self.refine_method_index = int(
                self.get_kwarg(kwargs, None, "refine_method_index",
                               "refine_method"))
        except ValueError:
            self.refine_method_index = self.refine_method_index
            pass  # ignore faulty values, these indices change from time to time.

        self.refine_methods = RefineMethodManager.initialize_methods(
            self.get_kwarg(kwargs, None, "refine_options"))

    # ------------------------------------------------------------
    #      Refiner methods
    # ------------------------------------------------------------
    def get_refiner(self):
        """
            This returns a Refiner object which can be used to refine the
            selected properties using the selected algorithm.
            Just call 'refine(stop)' on the returned object, with stop a
            threading.Event or multiprocessing.Event which you can use to stop
            the refinement before completion.
            The Refiner object also has a RefineHistory and RefineStatus object
            that can be used to track the status and history of the refinement.
        """

        return Refiner(method=self.get_refinement_method(),
                       data_callback=lambda: self.mixture.data_object,
                       refinables=self.refinables,
                       event_cmgr=EventContextManager(
                           self.mixture.needs_update,
                           self.mixture.data_changed),
                       metadata=dict(
                           phases=self.mixture.phases,
                           num_specimens=len(self.mixture.specimens),
                       ))

    # ------------------------------------------------------------
    #      Refinement Methods Management
    # ------------------------------------------------------------
    def get_refinement_method(self):
        """
            Returns the actual refinement method by translating the 
            `refine_method` attribute
        """
        return self.refine_methods[self.refine_method_index]

    # ------------------------------------------------------------
    #      Refinables Management
    # ------------------------------------------------------------
    # TODO set a restrict range attribute on the PropIntels, so we can use custom ranges for each property
    def auto_restrict(self):
        """
            Convenience function that restricts the selected properties 
            automatically by setting their minimum and maximum values.
        """
        with self.mixture.needs_update.hold():
            for node in self.refinables.iter_children():
                ref_prop = node.object
                if ref_prop.refine and ref_prop.refinable:
                    ref_prop.value_min = ref_prop.value * 0.8
                    ref_prop.value_max = ref_prop.value * 1.2

    def randomize(self):
        """
            Convenience function that randomize the selected properties.
            Respects the current minimum and maximum values.
            Executes an optimization after the randomization.
        """
        with self.mixture.data_changed.hold_and_emit():
            with self.mixture.needs_update.hold_and_emit():
                for node in self.refinables.iter_children():
                    ref_prop = node.object
                    if ref_prop.refine and ref_prop.refinable:
                        ref_prop.value = random.uniform(
                            ref_prop.value_min, ref_prop.value_max)

    def update_refinement_treestore(self):
        """
            This creates a tree store with all refinable properties and their
            minimum, maximum and current value.
        """
        if self.parent is not None:  # not linked so no valid phases!
            self.refinables.clear()

            def add_property(parent_node, obj, prop, is_grouper):
                rp = RefinableWrapper(obj=obj,
                                      prop=prop,
                                      parent=self.mixture,
                                      is_grouper=is_grouper)
                return parent_node.append(TreeNode(rp))

            def parse_attribute(obj, prop, root_node):
                """
                    obj: the object
                    attr: the attribute of obj or None if obj contains attributes
                    root_node: the root TreeNode new iters should be put under
                """
                if prop is not None:
                    if isinstance(prop, InheritableMixin):
                        value = prop.get_uninherited(obj)
                    else:
                        value = getattr(obj, prop.label)
                else:
                    value = obj

                if isinstance(
                        value,
                        RefinementValue):  # AtomRelation and UnitCellProperty
                    new_node = add_property(root_node, value, prop, False)
                elif hasattr(value, "__iter__"):  # List or similar
                    for new_obj in value:
                        parse_attribute(new_obj, None, root_node)
                elif isinstance(
                        value,
                        RefinementGroup):  # Phase, Component, Probability
                    if len(value.refinables) > 0:
                        new_node = add_property(root_node, value, prop, True)
                        for prop in value.refinables:
                            parse_attribute(value, prop, new_node)
                else:  # regular values
                    new_node = add_property(root_node, obj, prop, False)

            for phase in self.mixture.project.phases:
                if phase in self.mixture.phase_matrix:
                    parse_attribute(phase, None, self.refinables)

    pass  # end of class
Esempio n. 9
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