class _DummyObject(DataModel): name = StringProperty(text="Name", tabular=True, default="") number = FloatProperty(text="Number", tabular=True, default=0) test = LabeledProperty(text="Test", tabular=True, default=[]) pass # end of class
class _DummyParent(DataModel): attrib = LabeledProperty(text="Attrib", default=[], tabular=True, data_type=_DummyObject) pass # end of class
class AtomContentObject(Model): """ Wrapper around an atom object used in the AtomContents model. Stores the atom, the property to set and it's default amount. """ atom = LabeledProperty( default=None, text="Atom", visible=False, persistent=False, tabular=True, ) prop = LabeledProperty( default=None, text="Prop", visible=False, persistent=False, tabular=True, ) amount = FloatProperty( default=0.0, text="Amount", minimum=0.0, visible=False, persistent=False, tabular=True, ) def __init__(self, atom, prop, amount, *args, **kwargs): super(AtomContentObject, self).__init__(*args, **kwargs) self.atom = atom self.prop = prop self.amount = amount def update_atom(self, value): if not (self.atom == "" or self.atom is None or self.prop is None): with self.atom.data_changed.ignore(): setattr(self.atom, self.prop, self.amount * value) if hasattr(self.atom, "_set_driven_flag_for_prop"): self.atom._set_driven_flag_for_prop(self.prop) pass
class ChildModel(PyXRDModel): """ A PyXRDModel with child-parent relation support. """ # MODEL INTEL: class Meta(PyXRDModel.Meta): @classmethod def get_inheritable_properties( cls): # TODO MOVE THIS TO THE CHILD MODEL!! if not hasattr(cls, "all_properties"): raise RuntimeError("Meta class '%s' has not been initialized" \ " properly: 'all_properties' is not set!" % type(cls)) else: return [ attr for attr in cls.all_properties if getattr(attr, "inheritable", False) ] # SIGNALS: removed = SignalProperty() added = SignalProperty() # PROPERTIES: __parent = None def __get_parent(self): if callable(self.__parent): return self.__parent() else: return self.__parent def __set_parent(self, value): if not self.parent == value: if self.parent is not None: self.removed.emit() try: self.__parent = weakref.ref(value, self.__on_parent_finalize) except TypeError: self.__parent = value if self.parent is not None: self.added.emit() def __on_parent_finalize(self, ref): self.removed.emit() self.__parent = None parent = LabeledProperty(fget=__get_parent, fset=__set_parent) # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, parent=None, *args, **kwargs): super(ChildModel, self).__init__(*args, **kwargs) self.parent = parent pass # end of class
class ExperimentalLine(PyXRDLine): # MODEL INTEL: class Meta(PyXRDLine.Meta): store_id = "ExperimentalLine" specimen = property(DataModel.parent.fget, DataModel.parent.fset) # PROPERTIES: #: The line color color = modify(PyXRDLine.color, default=settings.EXPERIMENTAL_COLOR, inherit_from="parent.parent.display_exp_color") #: The linewidth in points lw = modify(PyXRDLine.lw, default=settings.EXPERIMENTAL_LINEWIDTH, inherit_from="parent.parent.display_exp_lw") #: A short string describing the (matplotlib) linestyle ls = modify(PyXRDLine.ls, default=settings.EXPERIMENTAL_LINESTYLE, inherit_from="parent.parent.display_exp_ls") #: A short string describing the (matplotlib) marker marker = modify(PyXRDLine.marker, default=settings.EXPERIMENTAL_MARKER, inherit_from="parent.parent.display_exp_marker") #: The value to cap the pattern at (in raw values) cap_value = FloatProperty(default=0.0, text="Cap value", persistent=True, visible=True, widget_type="float_entry", signal_name="visuals_changed", mix_with=(SignalMixin, )) @property def max_display_y(self): max_value = super(ExperimentalLine, self).max_display_y # Only cap single and multi-line patterns, not 2D images: if self.cap_value > 0 and not (self.num_columns > 2 and len(self.z_data)): max_value = min(max_value, self.cap_value) return max_value ########################################################################### #: The background offset value bg_position = FloatProperty(default=0.0, text="Background offset", persistent=False, visible=True, widget_type="float_entry", signal_name="visuals_changed", mix_with=(SignalMixin, )) #: The background scale bg_scale = FloatProperty(default=1.0, text="Background scale", persistent=False, visible=True, widget_type="float_entry", signal_name="visuals_changed", mix_with=(SignalMixin, )) #: The background pattern or None for linear patterns bg_pattern = LabeledProperty(default=None, text="Background pattern", persistent=False, visible=False, signal_name="visuals_changed", mix_with=(SignalMixin, )) #: The background type: pattern or linear bg_type = IntegerChoiceProperty(default=0, text="Background type", choices=settings.PATTERN_BG_TYPES, persistent=False, visible=True, signal_name="visuals_changed", set_action_name="find_bg_position", mix_with=( SignalMixin, SetActionMixin, )) def get_bg_type_lbl(self): return settings.PATTERN_BG_TYPES[self.bg_type] ########################################################################### #: Pattern smoothing type smooth_type = IntegerChoiceProperty( default=0, text="Smooth type", choices=settings.PATTERN_SMOOTH_TYPES, persistent=False, visible=True, signal_name="visuals_changed", set_action_name="setup_smooth_variables", mix_with=( SignalMixin, SetActionMixin, )) smooth_pattern = None #: The smooth degree smooth_degree = IntegerProperty(default=0, text="Smooth degree", persistent=False, visible=True, signal_name="visuals_changed", mix_with=(SignalMixin, )) ########################################################################### #: The noise fraction to add noise_fraction = FloatProperty(default=0.0, text="Noise fraction", persistent=False, visible=True, widget_type="spin", signal_name="visuals_changed", mix_with=(SignalMixin, )) ########################################################################### #: The pattern shift correction value shift_value = FloatProperty(default=0.0, text="Shift value", persistent=False, visible=True, widget_type="float_entry", signal_name="visuals_changed", mix_with=(SignalMixin, )) #: Shift reference position shift_position = FloatChoiceProperty( default=0.42574, text="Shift position", choices=settings.PATTERN_SHIFT_POSITIONS, persistent=False, visible=True, signal_name="visuals_changed", set_action_name="setup_shift_variables", mix_with=( SignalMixin, SetActionMixin, )) ########################################################################### #: The peak properties calculation start position peak_startx = FloatProperty(default=0.0, text="Peak properties start position", persistent=False, visible=True, widget_type="float_entry", set_action_name="update_peak_properties", mix_with=(SetActionMixin, )) #: The peak properties calculation end position peak_endx = FloatProperty(default=0.0, text="Peak properties end position", persistent=False, visible=True, widget_type="float_entry", set_action_name="update_peak_properties", mix_with=(SetActionMixin, )) #: The peak fwhm value peak_fwhm_result = FloatProperty( default=0.0, text="Peak FWHM value", persistent=False, visible=True, widget_type="label", ) #: The peak area value peak_area_result = FloatProperty( default=0.0, text="Peak area value", persistent=False, visible=True, widget_type="label", ) #: The patterns peak properties are calculated from peak_properties_pattern = LabeledProperty(default=None, text="Peak properties pattern", persistent=False, visible=False, signal_name="visuals_changed", mix_with=(SignalMixin, )) ########################################################################### #: The strip peak start position strip_startx = FloatProperty(default=0.0, text="Strip peak start position", persistent=False, visible=True, widget_type="float_entry", set_action_name="update_strip_pattern", mix_with=(SetActionMixin, )) #: The strip peak end position strip_endx = FloatProperty(default=0.0, text="Strip peak end position", persistent=False, visible=True, widget_type="float_entry", set_action_name="update_strip_pattern", mix_with=(SetActionMixin, )) #: The stripped peak pattern stripped_pattern = LabeledProperty(default=None, text="Strip peak pattern", persistent=False, visible=False, signal_name="visuals_changed", mix_with=(SignalMixin, )) #: The stripped peak pattern noise noise_level = FloatProperty(default=0.0, text="Strip peak noise level", persistent=False, visible=True, widget_type="float_entry", set_action_name="update_strip_pattern_noise", mix_with=(SetActionMixin, )) # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, cap_value=0.0, *args, **kwargs): """ Valid keyword arguments for a ExperimentalLine are: cap_value: the value (in raw counts) at which to cap the experimental pattern """ super(ExperimentalLine, self).__init__(*args, **kwargs) self.cap_value = cap_value # ------------------------------------------------------------ # Background Removal # ------------------------------------------------------------ def remove_background(self): with self.data_changed.hold_and_emit(): bg = None if self.bg_type == 0: bg = self.bg_position elif self.bg_type == 1 and self.bg_pattern is not None and not ( self.bg_position == 0 and self.bg_scale == 0): bg = self.bg_pattern * self.bg_scale + self.bg_position if bg is not None and self.data_y.size > 0: self.data_y[:, 0] -= bg self.clear_bg_variables() def find_bg_position(self): try: self.bg_position = np.min(self.data_y) except ValueError: return 0.0 def clear_bg_variables(self): with self.visuals_changed.hold_and_emit(): self.bg_pattern = None self.bg_scale = 0.0 self.bg_position = 0.0 # ------------------------------------------------------------ # Data Smoothing # ------------------------------------------------------------ def smooth_data(self): with self.data_changed.hold_and_emit(): if self.smooth_degree > 0: degree = int(self.smooth_degree) self.data_y[:, 0] = smooth(self.data_y[:, 0], degree) self.smooth_degree = 0.0 def setup_smooth_variables(self): with self.visuals_changed.hold_and_emit(): self.smooth_degree = 5.0 def clear_smooth_variables(self): with self.visuals_changed.hold_and_emit(): self.smooth_degree = 0.0 # ------------------------------------------------------------ # Noise adding # ------------------------------------------------------------ def add_noise(self): with self.data_changed.hold_and_emit(): if self.noise_fraction > 0: noisified = add_noise(self.data_y[:, 0], self.noise_fraction) self.set_data(self.data_x, noisified) self.noise_fraction = 0.0 def clear_noise_variables(self): with self.visuals_changed.hold_and_emit(): self.noise_fraction = 0.0 # ------------------------------------------------------------ # Data Shifting # ------------------------------------------------------------ def shift_data(self): with self.data_changed.hold_and_emit(): if self.shift_value != 0.0: if settings.PATTERN_SHIFT_TYPE == "Linear": self.data_x = self.data_x - self.shift_value if self.specimen is not None: with self.specimen.visuals_changed.hold(): for marker in self.specimen.markers: marker.position = marker.position - self.shift_value elif settings.PATTERN_SHIFT_TYPE == "Displacement": position = self.specimen.goniometer.get_t_from_nm( self.shift_position) displacement = 0.5 * self.specimen.goniometer.radius * self.shift_value / np.cos( position / 180 * np.pi) correction = 2 * displacement * np.cos( self.data_x / 2 / 180 * np.pi) / self.specimen.goniometer.radius self.data_x = self.data_x - correction self.shift_value = 0.0 def setup_shift_variables(self): with self.visuals_changed.hold_and_emit(): position = self.specimen.goniometer.get_2t_from_nm( self.shift_position) if position > 0.1: max_x = position + 0.5 min_x = position - 0.5 condition = (self.data_x >= min_x) & (self.data_x <= max_x) section_x, section_y = np.extract(condition, self.data_x), np.extract( condition, self.data_y[:, 0]) try: #TODO to exclude noise it'd be better to first interpolate # or smooth the data and then find the max. actual_position = section_x[np.argmax(section_y)] except ValueError: actual_position = position self.shift_value = actual_position - position def clear_shift_variables(self): with self.visuals_changed.hold_and_emit(): self.shift_value = 0 # ------------------------------------------------------------ # Peak area calculation # ------------------------------------------------------------ peak_bg_slope = 0.0 avg_starty = 0.0 avg_endy = 0.0 def update_peak_properties(self): with self.visuals_changed.hold_and_emit(): if self.peak_endx < self.peak_startx: self.peak_endx = self.peak_startx + 1.0 return # previous line will have re-invoked this method # calculate average starting point y value condition = (self.data_x >= self.peak_startx - 0.1) & ( self.data_x <= self.peak_startx + 0.1) section = np.extract(condition, self.data_y[:, 0]) self.avg_starty = np.min(section) # calculate average ending point y value condition = (self.data_x >= self.peak_endx - 0.1) & ( self.data_x <= self.peak_endx + 0.1) section = np.extract(condition, self.data_y[:, 0]) self.avg_endy = np.min(section) # Calculate new bg slope self.peak_bg_slope = (self.avg_starty - self.avg_endy) / ( self.peak_startx - self.peak_endx) # Get the x-values in between start and end point: condition = (self.data_x >= self.peak_startx) & (self.data_x <= self.peak_endx) section_x = np.extract(condition, self.data_x) section_y = np.extract(condition, self.data_y) bg_curve = (self.peak_bg_slope * (section_x - self.peak_startx) + self.avg_starty) #Calculate the peak area: self.peak_area_result = abs( trapz(section_y, x=section_x) - trapz(bg_curve, x=section_x)) # create a spline of of the peak (shifted down by half of its maximum) fwhm_curve = section_y - bg_curve peak_half_max = np.max(fwhm_curve) * 0.5 spline = UnivariateSpline(section_x, fwhm_curve - peak_half_max, s=0) roots = spline.roots() # find the roots = where the splin = 0 self.peak_fwhm_result = np.abs(roots[0] - roots[-1]) if ( len(roots) >= 2) else 0 # Calculate the new y-values: x values, bg_curve y values, original pattern y values, x values for the FWHM, y values for the FWHM self.peak_properties_pattern = (section_x, bg_curve, section_y, roots, spline(roots) + peak_half_max) def clear_peak_properties_variables(self): with self.visuals_changed.hold_and_emit(): self._peak_startx = 0.0 self._peak_properties_pattern = None self._peak_endx = 0.0 self.peak_properties = 0.0 # ------------------------------------------------------------ # Peak stripping # ------------------------------------------------------------ def strip_peak(self): with self.data_changed.hold_and_emit(): if self.stripped_pattern is not None: stripx, stripy = self.stripped_pattern indeces = ((self.data_x >= self.strip_startx) & (self.data_x <= self.strip_endx)).nonzero()[0] np.put(self.data_y[:, 0], indeces, stripy) self._strip_startx = 0.0 self._stripped_pattern = None self.strip_endx = 0.0 strip_slope = 0.0 avg_starty = 0.0 avg_endy = 0.0 block_strip = False def update_strip_pattern_noise(self): with self.visuals_changed.hold_and_emit(): # Get the x-values in between start and end point: condition = (self.data_x >= self.strip_startx) & (self.data_x <= self.strip_endx) section_x = np.extract(condition, self.data_x) # Calculate the new y-values, add noise according to noise_level noise = self.avg_endy * 2 * (np.random.rand(*section_x.shape) - 0.5) * self.noise_level section_y = (self.strip_slope * (section_x - self.strip_startx) + self.avg_starty) + noise self.stripped_pattern = (section_x, section_y) def update_strip_pattern(self): with self.visuals_changed.hold_and_emit(): if self.strip_endx < self.strip_startx: self.strip_endx = self.strip_startx + 1.0 return # previous line will have re-invoked this method if not self.block_strip: self.block_strip = True # calculate average starting point y value condition = (self.data_x >= self.strip_startx - 0.1) & ( self.data_x <= self.strip_startx + 0.1) section = np.extract(condition, self.data_y[:, 0]) self.avg_starty = np.average(section) noise_starty = 2 * np.std(section) / self.avg_starty # calculate average ending point y value condition = (self.data_x >= self.strip_endx - 0.1) & ( self.data_x <= self.strip_endx + 0.1) section = np.extract(condition, self.data_y[:, 0]) self.avg_endy = np.average(section) noise_endy = 2 * np.std(section) / self.avg_starty # Calculate new slope and noise level self.strip_slope = (self.avg_starty - self.avg_endy) / ( self.strip_startx - self.strip_endx) self.noise_level = (noise_starty + noise_endy) * 0.5 self.update_strip_pattern_noise() def clear_strip_variables(self): with self.visuals_changed.hold_and_emit(): self._strip_startx = 0.0 self._strip_pattern = None self.strip_start_x = 0.0 pass # end of class
class RawPatternPhase(RefinementGroup, AbstractPhase, metaclass=PyXRDRefinableMeta): # MODEL INTEL: class Meta(AbstractPhase.Meta): store_id = "RawPatternPhase" file_filters = [ ("Phase file", get_case_insensitive_glob("*.PHS")), ] rp_filters = xrd_parsers.get_import_file_filters() rp_export_filters = xrd_parsers.get_export_file_filters() _data_object = None @property def data_object(self): self._data_object.type = "RawPatternPhase" self._data_object.raw_pattern_x = self.raw_pattern.data_x self._data_object.raw_pattern_y = self.raw_pattern.data_y[:, 0] self._data_object.apply_lpf = False self._data_object.apply_correction = False return self._data_object project = property(AbstractPhase.parent.fget, AbstractPhase.parent.fset) @property def refine_title(self): return "Raw Pattern Phase" @property def is_refinable(self): return False @property def children_refinable(self): return False @property def refinables(self): return [] raw_pattern = LabeledProperty( default=None, text="Raw pattern", visible=True, persistent=True, tabular=True, inheritable=True, inherit_flag="inherit_CSDS_distribution", inherit_from="based_on.CSDS_distribution", signal_name="data_changed", mix_with=(SignalMixin, ObserveMixin,) ) @property def spec_max_display_y(self): """The maximum intensity (y-axis) of the current loaded profile""" _max = 0.0 if self.raw_pattern is not None: _max = max(_max, np.max(self.raw_pattern.max_display_y)) return _max # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): my_kwargs = self.pop_kwargs(kwargs, *[prop.label for prop in RawPatternPhase.Meta.get_local_persistent_properties()] ) super(RawPatternPhase, self).__init__(*args, **kwargs) kwargs = my_kwargs with self.data_changed.hold(): self.raw_pattern = PyXRDLine( data=self.get_kwarg(kwargs, None, "raw_pattern"), parent=self ) 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") def __repr__(self): return "RawPatternPhase(name='%s')" % (self.name) # ------------------------------------------------------------ # Notifications of observable properties # ------------------------------------------------------------ @AbstractPhase.observe("data_changed", signal=True) def notify_data_changed(self, model, prop_name, info): self.data_changed.emit() # propagate signal pass #end of class
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
class ThresholdSelector(ChildModel): # MODEL INTEL: specimen = property(DataModel.parent.fget, DataModel.parent.fset) # PROPERTIES: pattern = StringChoiceProperty( default="exp", text="Pattern", visible=True, persistent=False, choices={ "exp": "Experimental Pattern", "calc": "Calculated Pattern" }, set_action_name="update_threshold_plot_data", mix_with=(SetActionMixin, )) max_threshold = FloatProperty(default=0.32, text="Maximum threshold", visible=True, persistent=False, minimum=0.0, maximum=1.0, widget_type="float_entry", set_action_name="update_threshold_plot_data", mix_with=(SetActionMixin, )) steps = IntegerProperty(default=20, text="Steps", visible=True, persistent=False, minimum=3, maximum=50, set_action_name="update_threshold_plot_data", mix_with=(SetActionMixin, )) sel_num_peaks = IntegerProperty(default=0, text="Selected number of peaks", visible=True, persistent=False, widget_type="label") def set_sel_threshold(self, value): _sel_threshold = type(self).sel_threshold._get(self) if value != _sel_threshold and len(self.threshold_plot_data[0]) > 0: _sel_threshold = value if _sel_threshold >= self.threshold_plot_data[0][-1]: self.sel_num_peaks = self.threshold_plot_data[1][-1] elif _sel_threshold <= self.threshold_plot_data[0][0]: self.sel_num_peaks = self.threshold_plot_data[1][0] else: self.sel_num_peaks = int( interp1d(*self.threshold_plot_data)(_sel_threshold)) type(self).sel_threshold._set(self, _sel_threshold) sel_threshold = FloatProperty(default=0.1, text="Selected threshold", visible=True, persistent=False, widget_type="float_entry", fset=set_sel_threshold) threshold_plot_data = LabeledProperty(default=None, text="Threshold plot data", visible=False, persistent=False) # ------------------------------------------------------------ # Initialisation and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): super(ThresholdSelector, self).__init__(*args, **kwargs) self.max_threshold = self.get_kwarg(kwargs, self.max_threshold, "max_threshold") self.steps = self.get_kwarg(kwargs, self.steps, "steps") self.sel_threshold = self.get_kwarg(kwargs, self.sel_threshold, "sel_threshold") if self.parent.experimental_pattern.size > 0: self.pattern = "exp" else: self.pattern = "calc" # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def get_xy(self): if self.pattern == "exp": data_x, data_y = self.parent.experimental_pattern.get_xy_data() elif self.pattern == "calc": data_x, data_y = self.parent.calculated_pattern.get_xy_data() if data_y.size > 0: data_y = data_y / np.max(data_y) return data_x, data_y _updating_plot_data = False def update_threshold_plot_data(self, status_dict=None): if self.parent is not None and not self._updating_plot_data: self._updating_plot_data = True if self.pattern == "exp": p, t, m = self.parent.experimental_pattern.get_best_threshold( self.max_threshold, self.steps, status_dict) elif self.pattern == "calc": p, t, m = self.parent.calculated_pattern.get_best_threshold( self.max_threshold, self.steps, status_dict) self.threshold_plot_data = p self.sel_threshold = t self.max_threshold = m self._updating_plot_data = False pass # end of class
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
class Goniometer(DataModel, Storable): """ The Goniometer class contains all the information related to the X-ray diffraction goniometer, e.g. wavelength, radius, slit sizes, ... """ # MODEL INTEL: class Meta(DataModel.Meta): store_id = "Goniometer" _data_object = None @property def data_object(self): self._data_object.wavelength = self.wavelength x, y = self.wavelength_distribution.get_xy_data() self._data_object.wavelength_distribution = list( zip(x.tolist(), y.tolist())) return self._data_object specimen = property(DataModel.parent.fget, DataModel.parent.fset) # PROPERTIES: #: Start angle (in °2-theta, only used when calculating without #: experimental data) min_2theta = FloatProperty(default=3.0, text="Start angle", minimum=0.0, maximum=180.0, tabular=True, persistent=True, visible=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: End angle (in °2-theta, only used when calculating without #: experimental data) max_2theta = FloatProperty(default=3.0, text="End angle", minimum=0.0, maximum=180.0, tabular=True, persistent=True, visible=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: The number of steps between start and end angle steps = IntegerProperty(default=2500, text="Steps", minimum=0, maximum=10000, tabular=True, persistent=True, visible=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: The wavelength distribution wavelength_distribution = LabeledProperty(default=None, text="Wavelength distribution", tabular=False, persistent=True, visible=True, signal_name="data_changed", widget_type="xy_list_view", mix_with=(SignalMixin, ObserveMixin)) @FloatProperty(default=0.154056, text="Wavelength", tabular=True, persistent=False, visible=False, signal_name="data_changed", mix_with=(ReadOnlyMixin, )) def wavelength(self): """The wavelength of the generated X-rays (in nm)""" # Get the dominant wavelength in the distribution: x, y = self.wavelength_distribution.get_xy_data() wl = float(x[np.argmax(y)]) return wl #: Flag indicating if the first soller slit is present or not has_soller1 = BoolProperty(default=True, text="Soller 1", tabular=True, persistent=True, visible=True, signal_name="data_changed", mix_with=(DataMixin, SignalMixin)) #: The first Soller slit size (in °) soller1 = FloatProperty(default=2.3, text="Soller 1", minimum=0.0, maximum=10.0, tabular=True, persistent=True, visible=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: Flag indicating if the second soller slit is present or not has_soller2 = BoolProperty(default=True, text="Soller 2", tabular=True, persistent=True, visible=True, signal_name="data_changed", mix_with=(DataMixin, SignalMixin)) #: The second Soller slit size (in °) soller2 = FloatProperty(default=2.3, text="Soller 2", minimum=0.0, maximum=10.0, tabular=True, persistent=True, visible=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: The radius of the goniometer (in cm) radius = FloatProperty(default=24.0, text="Radius", minimum=0.0, maximum=200.0, tabular=True, persistent=True, visible=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: The divergence slit mode of the goniometer divergence_mode = StringChoiceProperty( default=settings.DEFAULT_DIVERGENCE_MODE, text="Divergence mode", visible=True, persistent=True, choices=settings.DIVERGENCE_MODES, signal_name="data_changed", mix_with=( DataMixin, SignalMixin, )) #: The divergence slit size (if fixed) or irradiated sample length (if automatic) divergence = FloatProperty(default=0.5, text="Divergence", minimum=0.0, maximum=90.0, tabular=True, persistent=True, visible=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: Flag indicating if the second soller slit is present or not has_absorption_correction = BoolProperty(default=False, text="Absorption correction", tabular=True, persistent=True, visible=True, signal_name="data_changed", mix_with=(DataMixin, SignalMixin)) #: The actual sample length sample_length = FloatProperty(default=1.25, text="Sample length [cm]", minimum=0.0, visible=True, persistent=True, tabular=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: The sample surface density sample_surf_density = FloatProperty(default=20.0, text="Sample surface density [mg/cm²]", minimum=0.0, visible=True, persistent=True, tabular=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: The sample mass absorption coefficient absorption = FloatProperty(default=45.0, text="Mass attenuation coeff. [cm²/g]", minimum=0.0, visible=True, persistent=True, tabular=True, signal_name="data_changed", widget_type="spin", mix_with=(DataMixin, SignalMixin)) #: Angular value (in degrees) for a monochromator correction - use 28.44 for silicon and 26.53 for carbon. mcr_2theta = FloatProperty(default=0.0, text="Monochromator 2θ", minimum=0.0, maximum=90.0, tabular=True, persistent=True, visible=True, signal_name="data_changed", widget_type="spin", mix_with=( DataMixin, SignalMixin, )) # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): """ Constructor takes any of its properties as a keyword argument. In addition to the above, the constructor still supports the following deprecated keywords, mapping to a current keyword: - lambda: maps to wavelength Any other arguments or keywords are passed to the base class. """ my_kwargs = self.pop_kwargs( kwargs, "data_radius", "data_divergence", "data_soller1", "data_soller2", "data_min_2theta", "data_max_2theta", "data_lambda", "lambda", "wavelength", "has_ads", "ads_fact", "ads_phase_fact", "ads_phase_shift", "ads_const", *[ prop.label for prop in Goniometer.Meta.get_local_persistent_properties() ]) super(Goniometer, self).__init__(*args, **kwargs) kwargs = my_kwargs self._data_object = GonioData() with self.data_changed.hold(): self.radius = self.get_kwarg(kwargs, 24.0, "radius", "data_radius") #: Parse divergence mode (including old-style keywords): new_div_mode = self.get_kwarg(kwargs, None, "divergence_mode") if new_div_mode is None: # old style project old_ads = self.get_kwarg(kwargs, None, "has_ads") if old_ads is not None and old_ads: # if we have ads, set as such: new_div_mode = "AUTOMATIC" else: # otherwise it was angular fixed slits new_div_mode = settings.DEFAULT_DIVERGENCE_MODE self.divergence_mode = new_div_mode # Divergence value: self.divergence = self.get_kwarg(kwargs, 0.5, "divergence", "data_divergence") # Monochromator correction: self.mcr_2theta = float(self.get_kwarg(kwargs, 0, "mcr_2theta")) # Soller slits: self.has_soller1 = self.get_kwarg(kwargs, True, "has_soller1") self.soller1 = float( self.get_kwarg(kwargs, 2.3, "soller1", "data_soller1")) self.has_soller2 = self.get_kwarg(kwargs, True, "has_soller2") self.soller2 = float( self.get_kwarg(kwargs, 2.3, "soller2", "data_soller2")) # Angular range settings for calculated patterns: self.min_2theta = float( self.get_kwarg(kwargs, 3.0, "min_2theta", "data_min_2theta")) self.max_2theta = float( self.get_kwarg(kwargs, 45.0, "max_2theta", "data_max_2theta")) self.steps = int(self.get_kwarg(kwargs, 2500, "steps")) # Sample characteristics self.sample_length = float( self.get_kwarg(kwargs, settings.DEFAULT_SAMPLE_LENGTH, "sample_length")) self.absorption = float(self.get_kwarg(kwargs, 45.0, "absorption")) self.sample_surf_density = float( self.get_kwarg(kwargs, 20.0, "sample_surf_density")) self.has_absorption_correction = bool( self.get_kwarg(kwargs, False, "has_absorption_correction")) wavelength = self.get_kwarg(kwargs, None, "wavelength", "data_lambda", "lambda") if not "wavelength_distribution" in kwargs and wavelength is not None: default_wld = [ [wavelength, 1.0], ] else: # A Cu wld: default_wld = [ [0.1544426, 0.955148885], [0.153475, 0.044851115], ] self.wavelength_distribution = StorableXYData( data=self.get_kwarg(kwargs, list(zip( *default_wld)), "wavelength_distribution")) # ------------------------------------------------------------ # Input/Output stuff # ------------------------------------------------------------ def __reduce__(self): return (type(self), ((), self.json_properties())) def json_properties(self): props = Storable.json_properties(self) props[ "wavelength_distribution"] = self.wavelength_distribution._serialize_data( ) return props def reset_from_file(self, gonfile): """ Loads & sets the parameters from the goniometer JSON file specified by `gonfile`, can be a filename or a file-like object. """ new_gonio = JSONParser.parse(gonfile) with self.data_changed.hold(): for prop in self.Meta.all_properties: if prop.persistent: if prop.label == "wavelength_distribution": self.wavelength_distribution.clear() self.wavelength_distribution.set_data( *new_gonio.wavelength_distribution.get_xy_data()) elif prop.label != "uuid": setattr(self, prop.label, getattr(new_gonio, prop.label)) # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def get_nm_from_t(self, theta): """Converts a theta position to a nanometer value""" return get_nm_from_t(theta, wavelength=self.wavelength, zero_for_inf=True) def get_nm_from_2t(self, twotheta): """Converts a 2-theta position to a nanometer value""" return get_nm_from_2t(twotheta, wavelength=self.wavelength, zero_for_inf=True) def get_t_from_nm(self, nm): """ Converts a nanometer value to a theta position""" return get_t_from_nm(nm, wavelength=self.wavelength) def get_2t_from_nm(self, nm): """ Converts a nanometer value to a 2-theta position""" return get_2t_from_nm(nm, wavelength=self.wavelength) def get_default_theta_range(self, as_radians=True): """ Returns a numpy array containing the theta values as radians from `min_2theta` to `max_2theta` with `steps` controlling the interval. When `as_radians` is set to False the values are returned as degrees. """ def torad(val): if as_radians: return radians(val) else: return val min_theta = torad(self.min_2theta * 0.5) max_theta = torad(self.max_2theta * 0.5) delta_theta = float(max_theta - min_theta) / float(self.steps) theta_range = (min_theta + delta_theta * np.arange( 0, self.steps, dtype=float)) + delta_theta * 0.5 return theta_range def get_lorentz_polarisation_factor(self, range_theta, sigma_star): """ Calculates Lorentz polarization factor for the given theta range and sigma-star value using the information about the goniometer's geometry. """ return get_lorentz_polarisation_factor(range_theta, sigma_star, self.soller1, self.soller2, self.mcr_2theta) def get_ADS_to_fixed_correction(self, range_theta): """ Returns a correction range that will convert ADS data to fixed slit data. Use with caution. """ return 1.0 / get_fixed_to_ads_correction_range(range_theta, self.data_object) pass # end of class
class Statistics(ChildModel): # PROPERTIES: specimen = property(ChildModel.parent.fget, ChildModel.parent.fset) @IntegerProperty(default=0, label="Points", visible=False, mix_with=(ReadOnlyMixin, )) def points(self): try: e_ex, e_ey, e_cx, e_cy = self.specimen.get_exclusion_xy( ) #@UnusedVariable return e_ex.size except: pass return 0 Rp = FloatProperty(default=None, label="Rp", visible=True) Rwp = FloatProperty(default=None, label="Rwp", visible=True) Rpder = FloatProperty(default=None, label="Rpder", visible=True) residual_pattern = LabeledProperty(default=None, label="Residual pattern") der_exp_pattern = LabeledProperty(default=None, label="Derived experimental pattern") der_calc_pattern = LabeledProperty(default=None, label="Derived calculated pattern") der_residual_pattern = LabeledProperty(default=None, label="Derived residual pattern") # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): super(Statistics, self).__init__(*args, **kwargs) self.observe_model(self.parent) # ------------------------------------------------------------ # Notifications of observable properties # ------------------------------------------------------------ @ChildModel.observe("parent", assign=True, after=True) def on_parent_changed(self, model, prop_name, info): self.update_statistics() # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def _get_experimental(self): if self.specimen is not None: x, y = self.specimen.experimental_pattern.get_xy_data() return x.copy(), y.copy() else: return None, None def _get_calculated(self): if self.specimen is not None: x, y = self.specimen.calculated_pattern.get_xy_data() return x.copy(), y.copy() else: return None, None def scale_factor_y(self, offset): return self.specimen.scale_factor_y(offset) if self.specimen else ( 1.0, offset) def update_statistics(self, derived=False): # Clear factors: self.Rp = 0 self.Rwp = 0 self.Rpder = 0 # Setup lines if not yet done: if self.residual_pattern == None: self.residual_pattern = PyXRDLine(label="Residual", color="#000000", lw=0.5, parent=self) if self.der_exp_pattern == None: self.der_exp_pattern = PyXRDLine(label="Exp. 1st der.", color="#000000", lw=2, parent=self) if self.der_calc_pattern == None: self.der_calc_pattern = PyXRDLine(label="Calc. 1st der.", color="#AA0000", lw=2, parent=self) if self.der_residual_pattern == None: self.der_residual_pattern = PyXRDLine(label="1st der. residual", color="#AA00AA", lw=1, parent=self) # Get data: exp_x, exp_y = self._get_experimental() cal_x, cal_y = self._get_calculated() der_exp_y, der_cal_y = None, None del cal_x # don't need this, is the same as exp_x # Try to get statistics, if it fails, just clear and inform the user try: if cal_y is not None and exp_y is not None and cal_y.size > 0 and exp_y.size > 0: # Get the selector for areas to consider in the statistics: selector = self.specimen.get_exclusion_selector() if derived: # Calculate and set first derivate patterns: der_exp_y, der_cal_y = derive(exp_y), derive(cal_y) self.der_exp_pattern.set_data(exp_x, der_exp_y) self.der_calc_pattern.set_data(exp_x, der_cal_y) # Calculate and set residual pattern: self.residual_pattern.set_data(exp_x, exp_y - cal_y) if derived: self.der_residual_pattern.set_data(exp_x, der_exp_y - der_cal_y) # Calculate 'included' R values: self.Rp = Rp(exp_y[selector], cal_y[selector]) self.Rwp = Rpw(exp_y[selector], cal_y[selector]) if derived: self.Rpder = Rp(der_exp_y[selector], der_cal_y[selector]) else: self.residual_pattern.clear() self.der_exp_pattern.clear() self.der_calc_pattern.clear() except: self.residual_pattern.clear() self.der_exp_pattern.clear() self.der_calc_pattern.clear() logger.error( "Error occurred when trying to calculate statistics, aborting calculation!" ) raise pass # end of class
class RefinableWrapper(ChildModel): """ Wrapper class for refinables easing the retrieval of certain properties for the different types of refinables. Can be used with an ObjectTreeStore. """ # MODEL INTEL: class Meta(ChildModel.Meta): parent_alias = "mixture" # PROPERTIES: #: The wrapped object obj = LabeledProperty(default=None, text="Wrapped object", tabular=True) #: The property descriptor object for the attribute prop_descr = LabeledProperty(default=None, text="Property descriptor", tabular=True) #: The Property label: @StringProperty(default="", text="Property label", tabular=True, mix_with=(ReadOnlyMixin, )) def label(self): return self.prop_descr.label #: A flag indicating whether this is wrapper is representing the group #: (True) or a member of the group (False): is_grouper = BoolProperty(default=False, text="Is grouper", tabular=True, mix_with=(ReadOnlyMixin, )) #: The inherit attribute name: @LabeledProperty(default=None, text="Inherit from label", mix_with=(ReadOnlyMixin, )) def inherit_from(self): return self.prop_descr.inherit_from if self.prop_descr else None #: The (possibly mathtext) label for the refinable property: @StringProperty(default="", text="Title", tabular=True, mix_with=(ReadOnlyMixin, )) def title(self): if (isinstance(self.obj, RefinementGroup) and self.is_grouper) or isinstance(self.obj, RefinementValue): return self.obj.refine_title else: if getattr(self.prop_descr, "math_text", None) is not None: return self.prop_descr.math_text else: return self.prop_descr.text #: The (pure text) label for the refinable property: @StringProperty(default="", text="Text title", tabular=True, mix_with=(ReadOnlyMixin, )) def text_title(self): if (isinstance(self.obj, RefinementGroup) and self.is_grouper) or isinstance(self.obj, RefinementValue): return self.obj.refine_title else: return self.prop_descr.text @StringProperty(default="", text="Descriptor", tabular=True, mix_with=(ReadOnlyMixin, )) def text_descriptor(self): """ Return a longer title that also describes this property's relations """ # This gets the phase and/or component name for the group or value: data = self.obj.refine_descriptor_data # Here we still need to get the actual property title: data["property_name"] = self.text_title return "%(phase_name)s | %(component_name)s | %(property_name)s" % data #: The actual value of the refinable property: @LabeledProperty(default=None, text="Value", tabular=True) def value(self): if isinstance(self.obj, RefinementValue): return self.obj.refine_value elif not self.is_grouper: return getattr(self.obj, self.label) else: return "" @value.setter def value(self, value): value = max(min(value, self.value_max), self.value_min) if self.is_grouper: raise AttributeError( "Cannot set the value for a grouping RefinableWrapper") elif isinstance(self.obj, RefinementValue): self.obj.refine_value = value else: setattr(self.obj, self.label, value) #: Whether or not this property is inherited from another object @BoolProperty(default=False, text="Inherited", tabular=True, mix_with=(ReadOnlyMixin, )) def inherited(self): return self.inherit_from is not None and hasattr( self.obj, self.inherit_from) and getattr(self.obj, self.inherit_from) #: Whether or not this property is actually refinable @BoolProperty(default=False, text="Refinable", tabular=True, mix_with=(ReadOnlyMixin, )) def refinable(self): if isinstance(self.obj, _RefinementBase): # We have a _RefinementBase property (group or value) if isinstance(self.obj, RefinementGroup): if self.is_grouper: # the grouper itself return False else: # attribute of the grouper return (not self.inherited) and self.obj.children_refinable elif isinstance(self.obj, RefinementValue): return (not self.inherited) and self.obj.is_refinable else: # This is actually impossible, but what the hack... return (not self.inherited) #: The refinement info object for the refinable property @LabeledProperty(default=None, text="Refinement info", tabular=True, mix_with=(ReadOnlyMixin, )) def ref_info(self): if (isinstance(self.obj, RefinementGroup) and self.is_grouper) or isinstance(self.obj, RefinementValue): return self.obj.refine_info else: name = self.prop_descr.get_refinement_info_name() if name is not None: ref_info = getattr(self.obj, name) return ref_info else: raise AttributeError( "Cannot find refine info model for attribute '%s' on '%s'" % (self.label, self.obj)) #: The minimum value for the refinable property @LabeledProperty(default=None, text="Minimum value", tabular=True) def value_min(self): return self.ref_info.minimum if self.ref_info else None @value_min.setter def value_min(self, value): if self.ref_info: self.ref_info.minimum = value #: The maximum value of the refinable property @LabeledProperty(default=None, text="Maximum value", tabular=True) def value_max(self): return self.ref_info.maximum if self.ref_info else None @value_max.setter def value_max(self, value): if self.ref_info: self.ref_info.maximum = value #: Wether this property is selected for refinement @BoolProperty(default=False, text="Refine", tabular=True) def refine(self): return self.ref_info.refine if self.ref_info else False @refine.setter def refine(self, value): if self.ref_info: self.ref_info.refine = value and self.refinable # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): """ Valid keyword arguments for a RefinableWrapper are: obj: the object we are wrapping a parameter for prop or prop_descr: the property descriptor is_grouper: whether or not this is a grouper object """ my_kwargs = self.pop_kwargs(kwargs, "obj", "prop", "prop_descr", "is_grouper") super(RefinableWrapper, self).__init__(**kwargs) kwargs = my_kwargs self.obj = self.get_kwarg(kwargs, None, "obj") self.prop_descr = self.get_kwarg(kwargs, None, "prop_descr", "prop") self._is_grouper = self.get_kwarg(kwargs, False, "is_grouper") pass # end of class
class UnitCellProperty(ComponentPropMixin, RefinementValue, DataModel, Storable, metaclass=PyXRDRefinableMeta): """ UnitCellProperty's are an integral part of a component and allow to calculate the dimensions of the unit cell based on compositional information such as the iron content. This class is not responsible for keeping its value up-to-date. With other words, it is the responsibility of the higher-level class to call the 'update_value' method on this object whenever it emits a 'data_changed' signal. The reason for this is to prevent infinite recursion errors. """ class Meta(DataModel.Meta): store_id = "UnitCellProperty" component = property(DataModel.parent.fget, DataModel.parent.fset) # PROPERTIES: #: The UnitCellProperty name name = StringProperty(default="", text="Name", visible=False, persistent=False, signal_name="visuals_changed", mix_with=(SignalMixin, )) #: Flag indicating if this UnitCellProperty is enabled enabled = BoolProperty(default=False, text="Enabled", visible=True, persistent=True, set_action_name="update_value", mix_with=(SetActionMixin, )) #: Flag indicating if this UnitCellProperty is inherited inherited = BoolProperty(default=False, text="Inherited", visible=False, persistent=False, set_action_name="update_value", mix_with=(SetActionMixin, )) #: The value of the UnitCellProperty value = FloatProperty(default=0.0, text="Value", visible=True, persistent=True, refinable=True, widget_type='float_entry', set_action_name="update_value", mix_with=(SetActionMixin, RefinableMixin)) #: The factor of the UnitCellProperty (if enabled and not constant) factor = FloatProperty(default=1.0, text="Factor", visible=True, persistent=True, widget_type='float_entry', set_action_name="update_value", mix_with=(SetActionMixin, )) #: The constant of the UnitCellProperty (if enabled and not constant) constant = FloatProperty(default=0.0, text="Constant", visible=True, persistent=True, widget_type='float_entry', set_action_name="update_value", mix_with=(SetActionMixin, )) _temp_prop = None # temporary, JSON-style prop prop = LabeledProperty(default=None, text="Property", visible=True, persistent=True, widget_type='combo', set_action_name="update_value", mix_with=(SetActionMixin, )) # REFINEMENT VALUE IMPLEMENTATION: @property def refine_title(self): return self.name @property def refine_descriptor_data(self): return dict(phase_name=self.component.phase.refine_title, component_name=self.component.refine_title) @property def refine_value(self): return self.value @refine_value.setter def refine_value(self, value): if not self.enabled: self.value = value @property def refine_info(self): return self.value_ref_info @property def is_refinable(self): return not (self.enabled or self.inherited) # ------------------------------------------------------------ # Initialisation and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): keys = [ prop.label for prop in UnitCellProperty.Meta.get_local_persistent_properties() ] keys.extend([ "data_%s" % prop.label for prop in UnitCellProperty.Meta.get_local_persistent_properties() ]) my_kwargs = self.pop_kwargs(kwargs, "name", *keys) super(UnitCellProperty, self).__init__(*args, **kwargs) kwargs = my_kwargs with self.data_changed.hold_and_emit(): self.name = self.get_kwarg(kwargs, self.name, "name", "data_name") self.value = self.get_kwarg(kwargs, self.value, "value", "data_value") self.factor = self.get_kwarg(kwargs, self.factor, "factor", "data_factor") self.constant = self.get_kwarg(kwargs, self.constant, "constant", "data_constant") self.enabled = self.get_kwarg(kwargs, self.enabled, "enabled", "data_enabled") self._temp_prop = self.get_kwarg(kwargs, self.prop, "prop", "data_prop") # ------------------------------------------------------------ # Input/Output stuff # ------------------------------------------------------------ def json_properties(self): retval = Storable.json_properties(self) if retval["prop"]: # Try to replace objects with their uuid's: try: retval["prop"] = [ getattr(retval["prop"][0], 'uuid', retval["prop"][0]), retval["prop"][1] ] except: logger.exception( "Error when trying to interpret UCP JSON properties") pass # ignore return retval def resolve_json_references(self): if getattr(self, "_temp_prop", None): self._temp_prop = list(self._temp_prop) if isinstance(self._temp_prop[0], str): obj = type(type(self)).object_pool.get_object( self._temp_prop[0]) if obj: self._temp_prop[0] = obj self.prop = self._temp_prop else: self._temp_prop = None self.prop = self._temp_prop del self._temp_prop # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def create_prop_store(self, extra_props=[]): assert (self.component is not None) store = Gtk.ListStore(object, str, str) # use private properties so we connect to the actual object stores and not the inherited ones for atom in self.component._layer_atoms: store.append([atom, "pn", atom.name]) for atom in self.component._interlayer_atoms: store.append([atom, "pn", atom.name]) for prop in extra_props: store.append(prop) return store def get_value_of_prop(self): try: return getattr(*self.prop) except: return 0.0 def update_value(self): if self.enabled: self._value = float(self.factor * self.get_value_of_prop() + self.constant) self.data_changed.emit() pass # end of class
class AtomRatio(AtomRelation): # MODEL INTEL: class Meta(AtomRelation.Meta): store_id = "AtomRatio" allowed_relations = { "AtomRatio": [ ("__internal_sum__", lambda o: "%s: SUM" % o.name), ("value", lambda o: "%s: RATIO" % o.name), ], "AtomContents": [("value", lambda o: o.name)], } # SIGNALS: # PROPERTIES: #: The sum of the two atoms sum = FloatProperty(default=1.0, text="Sum", minimum=0.0, visible=True, persistent=True, tabular=True, widget_type='float_entry', signal_name="data_changed", mix_with=(SignalMixin, )) def __internal_sum__(self, value): """ Special setter for other AtomRelation objects depending on the value of the sum of the AtomRatio. This can be used to have multi-substitution by linking two (or more) AtomRatio's. Eg Al-by-Mg-&-Fe: AtomRatioMgAndFeForAl -> links together Al content and Fe+Mg content => sum = e.g. 4 set by user AtomRatioMgForFe -> links together the Fe and Mg content => sum = set by previous ratio. """ self._sum = float(value) self.apply_relation() __internal_sum__ = property(fset=__internal_sum__) def _set_driven_flag_for_prop(self, prop, *args): """Internal method used to safely set the driven_by_other flag on an object. Subclasses can override to provide a check on the property set by the driver.""" if prop != "__internal_sum__": super(AtomRatio, self)._set_driven_flag_for_prop(prop) def _set_atom(self, value, label=None): if not self._safe_is_referring(value[0]): getattr(type(self), label)._set(self, value) #: The substituting atom atom1 = LabeledProperty(default=[None, None], text="Substituting Atom", visible=True, persistent=True, tabular=True, signal_name="data_changed", fset=partial(_set_atom, label="atom1"), mix_with=(SignalMixin, )) #: The Original atom atom2 = LabeledProperty(default=[None, None], text="Original Atom", visible=True, persistent=True, tabular=True, signal_name="data_changed", fset=partial(_set_atom, label="atom2"), mix_with=(SignalMixin, )) # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): # @ReservedAssignment """ Valid keyword arguments for an AtomRatio are: sum: the sum of the atoms contents atom1: a tuple containing the first atom and its property name to read/set atom2: a tuple containing the first atom and its property name to read/set The value property is the 'ratio' of the first atom over the sum of both """ my_kwargs = self.pop_kwargs( kwargs, "data_sum", "prop1", "data_prop1", "data_prop2", "prop2", *[ prop.label for prop in AtomRatio.Meta.get_local_persistent_properties() ]) super(AtomRatio, self).__init__(*args, **kwargs) kwargs = my_kwargs self.sum = self.get_kwarg(kwargs, self.sum, "sum", "data_sum") self._unresolved_atom1 = self._parseattr( self.get_kwarg(kwargs, [None, None], "atom1", "prop1", "data_prop1")) self._unresolved_atom2 = self._parseattr( self.get_kwarg(kwargs, [None, None], "atom2", "prop2", "data_prop2")) # ------------------------------------------------------------ # Input/Output stuff # ------------------------------------------------------------ def json_properties(self): retval = Storable.json_properties(self) retval["atom1"] = [ retval["atom1"][0].uuid if retval["atom1"][0] else None, retval["atom1"][1] ] retval["atom2"] = [ retval["atom2"][0].uuid if retval["atom2"][0] else None, retval["atom2"][1] ] return retval def resolve_relations(self): if isinstance(self._unresolved_atom1[0], str): self._unresolved_atom1[0] = type( type(self)).object_pool.get_object(self._unresolved_atom1[0]) self.atom1 = list(self._unresolved_atom1) del self._unresolved_atom1 if isinstance(self._unresolved_atom2[0], str): self._unresolved_atom2[0] = type( type(self)).object_pool.get_object(self._unresolved_atom2[0]) self.atom2 = list(self._unresolved_atom2) del self._unresolved_atom2 # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def apply_relation(self): if self.enabled and self.applicable: for value, (atom, prop) in [(self.value, self.atom1), (1.0 - self.value, self.atom2)]: if atom and prop: # do not fire events, just set attributes: with atom.data_changed.ignore(): setattr(atom, prop, value * self.sum) if hasattr(atom, "_set_driven_flag_for_prop"): atom._set_driven_flag_for_prop(prop) def iter_references(self): for atom in [self.atom1[0], self.atom2[0]]: yield atom pass # end of class
class _AbstractCSDSDistribution(DataModel, Storable, metaclass=PyXRDRefinableMeta): # MODEL INTEL: class Meta(DataModel.Meta): description = "Abstract CSDS distr." explanation = "" phase = property(DataModel.parent.fget, DataModel.parent.fset) # PROPERTIES: _data_object = None @property def data_object(self): return self._data_object inherited = BoolProperty(default=False, text="Inherited", visible=False, persistent=False, signal_name="data_changed", mix_with=(SignalMixin, )) distrib = LabeledProperty(default=None, text="CSDS Distribution", tabular=True, visible=False, persistent=False, get_action_name="_update_distribution", signal_name="data_changed", mix_with=( SignalMixin, GetActionMixin, )) # PROPERTIES: #: The maximum value of this distribution maximum = FloatProperty(default=0.0, text="Maximum CSDS", minimum=1, maximum=1000, tabular=True, persistent=False, visible=False, mix_with=(ReadOnlyMixin, DataMixin)) #: The minimum value of this distribution minimum = FloatProperty(default=0.0, text="Maximum CSDS", minimum=1, maximum=1000, tabular=True, persistent=False, visible=False, mix_with=(ReadOnlyMixin, DataMixin)) average = FloatProperty(default=0.0, text="Average CSDS", minimum=1, maximum=200, tabular=True, persistent=True, visible=True, refinable=True, signal_name="data_changed", set_action_name="_update_distribution", mix_with=(SignalMixin, DataMixin, RefinableMixin, SetActionMixin)) alpha_scale = FloatProperty(default=0.0, text="α scale factor", minimum=0.0, maximum=10.0, tabular=True, persistent=True, visible=True, refinable=True, signal_name="data_changed", set_action_name="_update_distribution", mix_with=(SignalMixin, DataMixin, RefinableMixin, SetActionMixin)) alpha_offset = FloatProperty(default=0.0, text="α offset factor", minimum=-5, maximum=5, tabular=True, persistent=True, visible=True, refinable=True, signal_name="data_changed", set_action_name="_update_distribution", mix_with=(SignalMixin, DataMixin, RefinableMixin, SetActionMixin)) beta_scale = FloatProperty(default=0.0, text="β² scale factor", minimum=0.0, maximum=10.0, tabular=True, persistent=True, visible=True, refinable=True, signal_name="data_changed", set_action_name="_update_distribution", mix_with=(SignalMixin, DataMixin, RefinableMixin, SetActionMixin)) beta_offset = FloatProperty(default=0.0, text="β² offset factor", minimum=-5, maximum=5, tabular=True, persistent=True, visible=True, refinable=True, signal_name="data_changed", set_action_name="_update_distribution", mix_with=(SignalMixin, DataMixin, RefinableMixin, SetActionMixin)) # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, average=10, alpha_scale=0.9485, alpha_offset=-0.0017, beta_scale=0.1032, beta_offset=0.0034, *args, **kwargs): super(_AbstractCSDSDistribution, self).__init__(*args, **kwargs) self._data_object = CSDSData() type(self).average._set(self, average) type(self).maximum._set( self, int(settings.LOG_NORMAL_MAX_CSDS_FACTOR * average)) type(self).minimum._set(self, 1) type(self).alpha_scale._set(self, alpha_scale) type(self).alpha_offset._set(self, alpha_offset) type(self).beta_scale._set(self, beta_scale) type(self).beta_offset._set(self, beta_offset) self._update_distribution() # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def _update_distribution(self): type(self).maximum._set( self, int(settings.LOG_NORMAL_MAX_CSDS_FACTOR * self.average)) self._distrib = calculate_distribution(self.data_object) pass # end of class
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
class _AbstractProbability(RefinementGroup, DataModel, Storable, metaclass=PyXRDRefinableMeta): # MODEL INTEL: class Meta(DataModel.Meta): pass phase = property(DataModel.parent.fget, DataModel.parent.fset) # PROPERTIES: name = StringProperty(default="Probabilities", text="Name") W_valid = LabeledProperty(default=None, text="Valid W matrix") W_valid_mask = None P_valid = LabeledProperty(default=None, text="Valid P matrix") P_valid_mask = None _R = -1 @property def R(self): return self._R @property def rank(self): return self.G**max(self.R, 1) _G = 0 @property def G(self): return self._G _W = None _P = None @IndexProperty def mP(self, indeces): r, ind = self._get_Pxy_from_indeces(indeces) return self._lP[r][ind] @mP.setter def mP(self, indeces, value): r, ind = self._get_Pxy_from_indeces(indeces) self._lP[r][ind] = value @IndexProperty def mW(self, indeces): r, ind = self._get_Wxy_from_indeces(indeces) return self._lW[r][ind] @mW.setter def mW(self, indeces, value): r, ind = self._get_Wxy_from_indeces(indeces) self._lW[r][ind] = value @property def _flags(self): return [ 1 if getattr(self, prop.inherit_flag, False) else 0 for prop in self.Meta.all_properties if getattr(prop, "inheritable", False) ] # REFINEMENT GROUP IMPLEMENTATION: @property def refine_title(self): return self.name @property def refine_descriptor_data(self): return dict(phase_name=self.phase.name, component_name="*") # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, R=-1, *args, **kwargs): super(_AbstractProbability, self).__init__(*args, **kwargs) self._R = R self._create_matrices() def _create_matrices(self): """ Creates a list of matrices for different 'levels' of R: e.g. when R=3 with g layers there can be 4 different W matrixes: Wi = gxg matrix Wij = g²xg² matrix Wijk = g³xg³ matrix Wijkl = another g³xg³ matrix (= Wijk * Pijkl) and 3 different P matrices: Pij = gxg matrix Pijk = g²xg² matrix Pijkl = g³xg³ matrix """ R = max(self.R, 1) self._lW = [None] * (R + 1) self._lP = [None] * R for r in range(R): lrank = self.G**(r + 1) self._lW[r] = np.zeros(shape=(lrank, lrank), dtype=float) self._lP[r] = np.zeros(shape=(lrank, lrank), dtype=float) self._lW[-1] = np.zeros(shape=(lrank, lrank), dtype=float) self._W = self._lW[-2] self._P = self._lP[-1] # validity matrices: self.W_valid = np.array([False] * (R + 1)) self.W_valid_mask = np.array([None] * (R + 1)) self.P_valid = np.array([False] * R) self.P_valid_mask = np.array([None] * R) # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def update(self): raise NotImplementedError def _clamp_set_and_update(self, name, value, minimum=0.0, maximum=1.0): clamped = min(max(value, minimum), maximum) if getattr(self, name) != clamped: setattr(self, name, clamped) self.update() def solve(self): """ This 'solves' the other W and P matrices using the 'top' P and W matrix calculated in the update method. """ for num in range(1, self.R): # W matrices: for base in product(list(range(self.G)), repeat=num): self.mW[base] = 0 for i in range(self.G): self.mW[base] += self.mW[(i, ) + base] # P matrices: p_num = num + 1 for base in product(list(range(self.G)), repeat=p_num): W = self.mW[base[:-1]] self.mP[base] = self.mW[base] / W if W > 0 else 0.0 # one extra W matrix: self._lW[-1][:] = np.dot(self._W, self._P) def validate(self): """ Checks wether the calculated matrices are valid, and stores the validation results in 'masks': matrices of the same size, in which the values correspond with 1 minus the number of validation rules that specific W or P value has failed for. """ def _validate_WW(W, R): """Validation rules for the product of a W and P matrix""" W_valid_mask = np.ones_like(W) rank = self.G**max(R, 1) # sum of the cols (W...x's) need to equal W... for i in range(rank): if abs(np.sum(W[..., i]) - self._W[i, i]) > 1e4: W_valid_mask[..., i] -= 1 # sum of the entire matrix must equal one: if abs(np.sum(W) - 1.0) > 1e4: W_valid_mask -= 1 # values need to be between 0 and 1 for i in range(rank): for j in range(rank): if W[i, j] < 0.0 or W[i, j] > 1.0: W_valid_mask[i, i] -= 1 # if the sum of the mask values equals the square of the rank, # no rules have been broken: W_valid = (np.sum(W_valid_mask) == rank**2) return W_valid, W_valid_mask def _validate_W(W, R): """Validation rules for a diagonal W matrix""" W_valid_mask = np.ones_like(W) rank = self.G**max(R, 1) # sum of the diagonal nees to be one if abs(np.sum(W) - 1.0) > 1e6: for i in range(rank): W_valid_mask[i, i] -= 1 # values need to be between 0 and 1 for i in range(rank): for j in range(rank): if W[i, j] < 0.0 or W[i, j] > 1.0: W_valid_mask[i, i] -= 1 # if the sum of the mask values equals the square of the rank, # no rules have been broken: W_valid = (np.sum(W_valid_mask) == rank**2) return W_valid, W_valid_mask def _validate_P(P, R): P_valid_mask = np.ones_like(P) rank = self.G**max(R, 1) # sum of the rows need to be one for i in range(rank): if abs(np.sum(P[i, ...]) - 1.0) > 1e6: P_valid_mask[i, ...] -= 1 # values need to be between 0 and 1 for i in range(rank): for j in range(rank): if P[i, j] < 0.0 or P[i, j] > 1.0: P_valid_mask[i, j] -= 1 # if the sum of the mask values equals the square of the rank, # no rules have been broken: P_valid = (np.sum(P_valid_mask) == rank**2) return P_valid, P_valid_mask for i in range(max(self.R, 1)): self.W_valid[i], self.W_valid_mask[i] = _validate_W( self._lW[i], i + 1) self.P_valid[i], self.P_valid_mask[i] = _validate_P( self._lP[i], i + 1) # the extra W matrix validates differently: self.W_valid[-1], self.W_valid_mask[-1] = _validate_WW( self._lW[-1], self.R) def _get_Pxy_from_indeces(self, indeces): if not hasattr(indeces, "__iter__"): indeces = [indeces] l = len(indeces) assert ( l > 1 ), "Two or more indeces needed to acces P elements, not %s" % indeces assert (l <= max(self.R, 1) + 1), "Too many indeces for an R%d model: %s" % (self.R, indeces) R = max(l - 1, 1) x, y = 0, 0 for i in range(1, R + 1): f = self.G**(R - i) x += indeces[i - 1] * f y += indeces[i] * f return (l - 2), (x, y) def _get_Wxy_from_indeces(self, indeces): if not hasattr(indeces, "__iter__"): indeces = [indeces] l = len(indeces) assert (l > 0), "One or more indeces needed to acces W elements" assert (l <= max(self.R, 1) + 1), "Too many indeces for an R%d model: %s" % (self.R, indeces) if l == (max(self.R, 1) + 1): R = max(l - 1, 1) x, y = 0, 0 for i in range(1, R + 1): f = self.G**(R - i) x += indeces[i - 1] * f y += indeces[i] * f return (l - 1), (x, y) else: R = max(l, 1) x = 0 for i in range(R): x += indeces[i] * self.G**(R - (i + 1)) return (l - 1), (x, x) def get_all_matrices(self): return self._lW, self._lP def get_distribution_matrix(self): return self._W def get_distribution_array(self): return np.diag(self._W) def get_probability_matrix(self): return self._P _stashed_lP = None _stashed_lW = None _stashed_flags = None def _stash_matrices(self): """ Stashes the matrices for an update """ self._stashed_lW = deepcopy(np.asanyarray(self._lW)) self._stashed_lP = deepcopy(np.asanyarray(self._lP)) self._stashed_flags = deepcopy(np.asarray(self._flags)) def _compare_stashed_matrices(self): """ Unstashed matrices and compares with current values, if identical returns True """ if self._stashed_lP is not None and self._stashed_lW is not None: result = np.array_equal(self._stashed_lW, np.asanyarray(self._lW)) result = result and np.array_equal(self._stashed_lP, np.asanyarray(self._lP)) result = result and np.array_equal(self._stashed_flags, np.asanyarray(self._flags)) self._stashed_lW = None self._stashed_lP = None return result else: return False @contextmanager def monitor_changes(self): with self.data_changed.hold(): self._stash_matrices() yield if not self._compare_stashed_matrices(): self.data_changed.emit() pass # end of class