class CalculatedLine(PyXRDLine): # MODEL INTEL: class Meta(PyXRDLine.Meta): store_id = "CalculatedLine" inherit_format = "display_calc_%s" specimen = property(DataModel.parent.fget, DataModel.parent.fset) # PROPERTIES: phase_colors = ListProperty( default=[], test="Phase colors", mix_with=(SignalMixin, ), signal_name="visuals_changed", ) #: The line color color = modify(PyXRDLine.color, default=settings.CALCULATED_COLOR, inherit_from="parent.parent.display_calc_color") #: The linewidth in points lw = modify(PyXRDLine.lw, default=settings.CALCULATED_LINEWIDTH, inherit_from="parent.parent.display_calc_lw") #: A short string describing the (matplotlib) linestyle ls = modify(PyXRDLine.ls, default=settings.CALCULATED_LINESTYLE, inherit_from="parent.parent.display_calc_ls") #: A short string describing the (matplotlib) marker marker = modify(PyXRDLine.marker, default=settings.CALCULATED_MARKER, inherit_from="parent.parent.display_calc_marker") pass # end of class
class PyXRDLine(StorableXYData): """ A PyXRDLine is an abstract attribute holder for a real 'Line' object, whatever the plotting library used may be. Attributes are line width and color. """ # MODEL INTEL: class Meta(StorableXYData.Meta): store_id = "PyXRDLine" # OBSERVABLE PROPERTIES: #: The line label label = StringProperty(default="", text="Label", persistent=True) #: The line color color = StringProperty(default="#000000", text="Label", visible=True, persistent=True, widget_type="color", inherit_flag="inherit_color", inherit_from="parent.parent.display_exp_color", signal_name="visuals_changed", mix_with=(InheritableMixin, SignalMixin)) #: Flag indicating whether to use the grandparents color yes/no inherit_color = BoolProperty(default=True, text="Inherit color", visible=True, persistent=True, signal_name="visuals_changed", mix_with=(SignalMixin, )) #: The linewidth in points lw = FloatProperty( default=2.0, text="Linewidth", visible=True, persistent=True, widget_type="spin", inherit_flag="inherit_lw", inherit_from="parent.parent.display_exp_lw", signal_name="visuals_changed", mix_with=(InheritableMixin, SignalMixin), ) #: Flag indicating whether to use the grandparents linewidth yes/no inherit_lw = BoolProperty( default=True, text="Inherit linewidth", visible=True, persistent=True, signal_name="visuals_changed", mix_with=(SignalMixin, ), ) #: A short string describing the (matplotlib) linestyle ls = StringChoiceProperty( default=settings.EXPERIMENTAL_LINESTYLE, text="Linestyle", visible=True, persistent=True, choices=settings.PATTERN_LINE_STYLES, mix_with=( InheritableMixin, SignalMixin, ), signal_name="visuals_changed", inherit_flag="inherit_ls", inherit_from="parent.parent.display_exp_ls", ) #: Flag indicating whether to use the grandparents linestyle yes/no inherit_ls = BoolProperty(default=True, text="Inherit linestyle", visible=True, persistent=True, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: A short string describing the (matplotlib) marker marker = StringChoiceProperty( default=settings.EXPERIMENTAL_MARKER, text="Marker", visible=True, persistent=True, choices=settings.PATTERN_MARKERS, mix_with=( InheritableMixin, SignalMixin, ), signal_name="visuals_changed", inherit_flag="inherit_marker", inherit_from="parent.parent.display_exp_marker", ) #: Flag indicating whether to use the grandparents linewidth yes/no inherit_marker = BoolProperty( default=True, text="Inherit marker", visible=True, persistent=True, mix_with=(SignalMixin, ), signal_name="visuals_changed", ) #: z-data (e.g. relative humidity, temperature, for multi-column 'lines') z_data = ListProperty(default=None, text="Z data", data_type=float, persistent=True, visible=False) # REGULAR PROPERTIES: @property def max_display_y(self): if self.num_columns > 2: # If there's several y-columns, check if we have z-data associated with them # if so, it is a 2D pattern, otherwise this is a multi-line pattern if len(self.z_data) > 2: return np.max(self.z_data) else: return self.max_y else: # If there's a single comumn of y-data, just get the max value return self.max_y @property def min_intensity(self): if self.num_columns > 2: return np.min(self.z_data) else: return self.min_y @property def abs_max_intensity(self): return self.abs_max_y # ------------------------------------------------------------ # Initialisation and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): """ Valid keyword arguments for a PyXRDLine are: data: the actual data containing x and y values label: the label for this line color: the color of this line inherit_color: whether to use the parent-level color or its own lw: the line width of this line inherit_lw: whether to use the parent-level line width or its own ls: the line style of this line inherit_ls: whether to use the parent-level line style or its own marker: the line marker of this line inherit_marker: whether to use the parent-level line marker or its own z_data: the z-data associated with the columns in a multi-column pattern """ my_kwargs = self.pop_kwargs( kwargs, *[ prop.label for prop in PyXRDLine.Meta.get_local_persistent_properties() ]) super(PyXRDLine, self).__init__(*args, **kwargs) kwargs = my_kwargs with self.visuals_changed.hold(): self.label = self.get_kwarg(kwargs, self.label, "label") self.color = self.get_kwarg(kwargs, self.color, "color") self.inherit_color = bool( self.get_kwarg(kwargs, self.inherit_color, "inherit_color")) self.lw = float(self.get_kwarg(kwargs, self.lw, "lw")) self.inherit_lw = bool( self.get_kwarg(kwargs, self.inherit_lw, "inherit_lw")) self.ls = self.get_kwarg(kwargs, self.ls, "ls") self.inherit_ls = bool( self.get_kwarg(kwargs, self.inherit_ls, "inherit_ls")) self.marker = self.get_kwarg(kwargs, self.marker, "marker") self.inherit_marker = bool( self.get_kwarg(kwargs, self.inherit_marker, "inherit_marker")) self.z_data = list(self.get_kwarg(kwargs, [0], "z_data")) # ------------------------------------------------------------ # Input/Output stuff # ------------------------------------------------------------ @classmethod def from_json(cls, **kwargs): # @ReservedAssignment if "xy_store" in kwargs: if "type" in kwargs["xy_store"]: kwargs["data"] = kwargs["xy_store"]["properties"]["data"] elif "xy_data" in kwargs: if "type" in kwargs["xy_data"]: kwargs["data"] = kwargs["xy_data"]["properties"]["data"] kwargs["label"] = kwargs["data_label"] del kwargs["data_name"] del kwargs["data_label"] del kwargs["xy_data"] return cls(**kwargs) # ------------------------------------------------------------ # Convenience Methods & Functions # ------------------------------------------------------------ def interpolate(self, *x_vals, **kwargs): """ Returns a list of (x, y) tuples for the passed x values. An optional column keyword argument can be passed to select a column, by default the first y-column is used. Returned y-values are interpolated. """ column = kwargs.get("column", 0) f = interp1d(self.data_x, self.data_y[:, column]) return list(zip(x_vals, f(x_vals))) def get_plotted_y_at_x(self, x): """ Gets the (interpolated) plotted value at the given x position. If this line has not been plotted (or does not have access to a '__plot_line' attribute set by the plotting routines) it will return 0. """ try: xdata, ydata = getattr(self, "__plot_line").get_data() except AttributeError: logging.exception( "Attribute error when trying to get plotter data at x position!" ) else: if len(xdata) > 0 and len(ydata) > 0: return np.interp(x, xdata, ydata) return 0 def calculate_npeaks_for(self, max_threshold, steps): """ Calculates the number of peaks for `steps` threshold values between 0 and `max_threshold`. Returns a tuple containing two lists with the threshold values and the corresponding number of peaks. """ length = self.data_x.size resolution = length / (self.data_x[-1] - self.data_x[0]) delta_angle = 0.05 window = int(delta_angle * resolution) window += (window % 2) * 2 steps = max(steps, 2) - 1 factor = max_threshold / steps deltas = [i * factor for i in range(0, steps)] numpeaks = [] maxtabs, mintabs = multi_peakdetect(self.data_y[:, 0], self.data_x, 5, deltas) for maxtab, _ in zip(maxtabs, mintabs): numpeak = len(maxtab) numpeaks.append(numpeak) numpeaks = list(map(float, numpeaks)) return deltas, numpeaks def get_best_threshold(self, max_threshold=None, steps=None, status_dict=None): """ Estimates the best threshold for peak detection using an iterative algorithm. Assumes there is a linear contribution from noise. Returns a 4-tuple containing the selected threshold, the maximum threshold, a list of threshold values and a list with the corresponding number of peaks. """ length = self.data_x.size steps = not_none(steps, 20) threshold = 0.1 max_threshold = not_none(max_threshold, threshold * 3.2) def get_new_threshold(threshold, deltas, num_peaks, ln): # Left side line: x = deltas[:ln] y = num_peaks[:ln] slope, intercept, R, _, _ = stats.linregress(x, y) return R, -intercept / slope if length > 2: # Adjust the first distribution: deltas, num_peaks = self.calculate_npeaks_for(max_threshold, steps) # Fit several lines with increasing number of points from the # generated threshold / marker count graph. Stop when the # R-coefficiënt drops below 0.95 (past linear increase from noise) # Then repeat this by increasing the resolution of data points # and continue until the result does not change anymore last_threshold = None solution = False max_iters = 10 min_iters = 3 itercount = 0 if status_dict is not None: status_dict["progress"] = 0 while not solution: # Number of points to use for the lin regress: ln = 4 # Maximum number of points to use: max_ln = len(deltas) # Flag indicating if we can stop searching for the linear part stop = False while not stop: R, threshold = get_new_threshold(threshold, deltas, num_peaks, ln) max_threshold = threshold * 3.2 if abs(R) < 0.98 or ln >= max_ln: stop = True else: ln += 1 itercount += 1 # Increase # of iterations if last_threshold: # Check if we have run at least `min_iters`, at most `max_iters` # and have not reached an equilibrium. solution = bool( itercount > min_iters and not (itercount <= max_iters and last_threshold - threshold >= 0.001)) if not solution: deltas, num_peaks = self.calculate_npeaks_for( max_threshold, steps) last_threshold = threshold if status_dict is not None: status_dict["progress"] = float(itercount / max_iters) return (deltas, num_peaks), threshold, max_threshold else: return ([], []), threshold, max_threshold pass # end of class
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 MineralScorer(DataModel): specimen = property(DataModel.parent.fget, DataModel.parent.fset) matches_changed = SignalProperty() matches = ListProperty(default=None, text="Matches", visible=True, persistent=False, mix_with=(ReadOnlyMixin, )) @ListProperty(default=None, text="Minerals", visible=True, persistent=False, mix_with=(ReadOnlyMixin, )) def minerals(self): # Load them when accessed for the first time: _minerals = type(self).minerals._get(self) if _minerals == None: _minerals = list() with unicode_open( settings.DATA_REG.get_file_path("MINERALS")) as f: mineral = "" abbreviation = "" position_flag = True peaks = [] for line in f: line = line.replace('\n', '') try: number = float(line) if position_flag: position = number else: intensity = number peaks.append((position, intensity)) position_flag = not position_flag except ValueError: if mineral != "": _minerals.append((mineral, abbreviation, peaks)) position_flag = True if len(line) > 25: mineral = line[:24].strip() if len(line) > 49: abbreviation = line[49:].strip() peaks = [] sorted(_minerals, key=lambda mineral: mineral[0]) type(self).minerals._set(self, _minerals) return _minerals # ------------------------------------------------------------ # Initialisation and other internals # ------------------------------------------------------------ def __init__(self, marker_peaks=[], *args, **kwargs): super(MineralScorer, self).__init__(*args, **kwargs) self._matches = [] self.marker_peaks = marker_peaks # position, intensity # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def auto_match(self): self._matches = score_minerals(self.marker_peaks, self.minerals) self.matches_changed.emit() def del_match(self, index): if self.matches: del self.matches[index] self.matches_changed.emit() def add_match(self, name, abbreviation, peaks): matches = score_minerals(self.marker_peaks, [(name, abbreviation, peaks)]) if len(matches): name, abbreviation, peaks, matches, score = matches[0] else: matches, score = [], 0. self.matches.append([name, abbreviation, peaks, matches, score]) sorted(self._matches, key=lambda match: match[-1], reverse=True) self.matches_changed.emit() pass # end of class
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 AtomContents(AtomRelation): # MODEL INTEL: class Meta(AtomRelation.Meta): store_id = "AtomContents" allowed_relations = { "AtomRatio": [ ("__internal_sum__", lambda o: "%s: SUM" % o.name), ("value", lambda o: "%s: RATIO" % o.name), ], } # SIGNALS: # PROPERTIES: atom_contents = ListProperty(default=None, text="Atom contents", visible=True, persistent=True, tabular=True, data_type=AtomContentObject, mix_with=(ReadOnlyMixin, )) # ------------------------------------------------------------ # Initialisation and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): """ Valid keyword arguments for an AtomContents are: atom_contents: a list of tuples containing the atom content object uuids, property names and default amounts """ my_kwargs = self.pop_kwargs( kwargs, *[ prop.label for prop in AtomContents.Meta.get_local_persistent_properties() ]) super(AtomContents, self).__init__(*args, **kwargs) kwargs = my_kwargs # Load atom contents: atom_contents = [] for uuid, prop, amount in self.get_kwarg(kwargs, [], "atom_contents"): # uuid's are resolved when resolve_relations is called atom_contents.append(AtomContentObject(uuid, prop, amount)) type(self).atom_contents._set(self, atom_contents) def on_change(*args): if self.enabled: # no need for updates otherwise self.data_changed.emit() self._atom_contents_observer = ListObserver(on_change, on_change, prop_name="atom_contents", model=self) # ------------------------------------------------------------ # Input/Output stuff # ------------------------------------------------------------ def json_properties(self): retval = Storable.json_properties(self) retval["atom_contents"] = list([[ atom_contents.atom.uuid if atom_contents.atom else None, atom_contents.prop, atom_contents.amount ] for atom_contents in retval["atom_contents"]]) return retval def resolve_relations(self): # Disable event dispatching to prevent infinite loops enabled = self.enabled self.enabled = False # Change rows with string references to objects (uuid's) for atom_content in self.atom_contents: if isinstance(atom_content.atom, str): atom_content.atom = type(type(self)).object_pool.get_object( atom_content.atom) # Set the flag to its original value self.enabled = enabled # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def apply_relation(self): if self.enabled and self.applicable: for atom_content in self.atom_contents: atom_content.update_atom(self.value) def set_atom_content_values(self, path, new_atom, new_prop): """ Convenience function that first checks if the new atom value will not cause a circular reference before actually setting it. """ with self.data_changed.hold(): atom_content = self.atom_contents[int(path[0])] if atom_content.atom != new_atom: old_atom = atom_content.atom atom_content.atom = None # clear... if not self._safe_is_referring(new_atom): atom_content.atom = new_atom else: atom_content.atom = old_atom else: atom_content.atom = None atom_content.prop = new_prop def iter_references(self): for atom_content in self.atom_contents: yield atom_content.atom pass # end of class
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 Refinement(ChildModel): """ A simple model that plugs onto the Mixture model. It provides the functionality related to refinement of parameters. """ # MODEL INTEL: class Meta(ChildModel.Meta): store_id = "Refinement" mixture = property(ChildModel.parent.fget, ChildModel.parent.fset) #: Flag, True if after refinement plots should be generated of the parameter space make_psp_plots = BoolProperty(default=False, text="Make parameter space plots", tabular=False, visible=True, persistent=True) #: TreeNode containing the refinable properties refinables = ListProperty(default=None, text="Refinables", tabular=True, persistent=False, visible=True, data_type=RefinableWrapper, cast_to=None, widget_type="object_tree_view") #: A dict containing an instance of each refinement method refine_methods = None #: An integer describing which method to use for the refinement refine_method_index = IntegerChoiceProperty( default=0, text="Refinement method index", tabular=True, persistent=True, visible=True, choices={ key: method.name for key, method in RefineMethodManager.get_all_methods().items() }) #: A dict containing the current refinement options @LabeledProperty(default=None, text="Refine options", persistent=False, visible=False, mix_with=(ReadOnlyMixin, )) def refine_options(self): return self.get_refinement_method().get_options() #: A dict containing all refinement options @property def all_refine_options(self): return { method.index: method.get_options() for method in list(self.refine_methods.values()) } def __init__(self, *args, **kwargs): my_kwargs = self.pop_kwargs(kwargs, "refine_method_index", "refine_method", "refine_options") super(Refinement, self).__init__(*args, **kwargs) kwargs = my_kwargs # Setup the refinables treestore self.refinables = TreeNode() self.update_refinement_treestore() # Setup the refine methods try: self.refine_method_index = int( self.get_kwarg(kwargs, None, "refine_method_index", "refine_method")) except ValueError: self.refine_method_index = self.refine_method_index pass # ignore faulty values, these indices change from time to time. self.refine_methods = RefineMethodManager.initialize_methods( self.get_kwarg(kwargs, None, "refine_options")) # ------------------------------------------------------------ # Refiner methods # ------------------------------------------------------------ def get_refiner(self): """ This returns a Refiner object which can be used to refine the selected properties using the selected algorithm. Just call 'refine(stop)' on the returned object, with stop a threading.Event or multiprocessing.Event which you can use to stop the refinement before completion. The Refiner object also has a RefineHistory and RefineStatus object that can be used to track the status and history of the refinement. """ return Refiner(method=self.get_refinement_method(), data_callback=lambda: self.mixture.data_object, refinables=self.refinables, event_cmgr=EventContextManager( self.mixture.needs_update, self.mixture.data_changed), metadata=dict( phases=self.mixture.phases, num_specimens=len(self.mixture.specimens), )) # ------------------------------------------------------------ # Refinement Methods Management # ------------------------------------------------------------ def get_refinement_method(self): """ Returns the actual refinement method by translating the `refine_method` attribute """ return self.refine_methods[self.refine_method_index] # ------------------------------------------------------------ # Refinables Management # ------------------------------------------------------------ # TODO set a restrict range attribute on the PropIntels, so we can use custom ranges for each property def auto_restrict(self): """ Convenience function that restricts the selected properties automatically by setting their minimum and maximum values. """ with self.mixture.needs_update.hold(): for node in self.refinables.iter_children(): ref_prop = node.object if ref_prop.refine and ref_prop.refinable: ref_prop.value_min = ref_prop.value * 0.8 ref_prop.value_max = ref_prop.value * 1.2 def randomize(self): """ Convenience function that randomize the selected properties. Respects the current minimum and maximum values. Executes an optimization after the randomization. """ with self.mixture.data_changed.hold_and_emit(): with self.mixture.needs_update.hold_and_emit(): for node in self.refinables.iter_children(): ref_prop = node.object if ref_prop.refine and ref_prop.refinable: ref_prop.value = random.uniform( ref_prop.value_min, ref_prop.value_max) def update_refinement_treestore(self): """ This creates a tree store with all refinable properties and their minimum, maximum and current value. """ if self.parent is not None: # not linked so no valid phases! self.refinables.clear() def add_property(parent_node, obj, prop, is_grouper): rp = RefinableWrapper(obj=obj, prop=prop, parent=self.mixture, is_grouper=is_grouper) return parent_node.append(TreeNode(rp)) def parse_attribute(obj, prop, root_node): """ obj: the object attr: the attribute of obj or None if obj contains attributes root_node: the root TreeNode new iters should be put under """ if prop is not None: if isinstance(prop, InheritableMixin): value = prop.get_uninherited(obj) else: value = getattr(obj, prop.label) else: value = obj if isinstance( value, RefinementValue): # AtomRelation and UnitCellProperty new_node = add_property(root_node, value, prop, False) elif hasattr(value, "__iter__"): # List or similar for new_obj in value: parse_attribute(new_obj, None, root_node) elif isinstance( value, RefinementGroup): # Phase, Component, Probability if len(value.refinables) > 0: new_node = add_property(root_node, value, prop, True) for prop in value.refinables: parse_attribute(value, prop, new_node) else: # regular values new_node = add_property(root_node, obj, prop, False) for phase in self.mixture.project.phases: if phase in self.mixture.phase_matrix: parse_attribute(phase, None, self.refinables) pass # end of class
class Project(DataModel, Storable): """ This is the top-level object that servers the purpose of combining the different objects (most notably :class:`~.atoms.models.AtomType`'s, :class:`~.phases.models.Phase`'s, :class:`~.specimen.models.Specimen`'s and :class:`~.mixture.models.Mixture`'s). It also provides a large number of display-related 'default' properties (e.g. for patterns and their markers, axes etc.). For more details: see the property descriptions. Example usage: .. code-block:: python >>> from pyxrd.project.models import Project >>> from pyxrd.generic.io.xrd_parsers import XRDParser >>> from pyxrd.specimen.models import Specimen >>> project = Project(name="New Project", author="Mr. X", layout_mode="FULL", axes_dspacing=True) >>> for specimen in Specimen.from_experimental_data("/path/to/xrd_data_file.rd", parent=project): ... project.specimens.append(specimen) ... """ # MODEL INTEL: class Meta(DataModel.Meta): store_id = "Project" file_filters = [ ("PyXRD Project files", get_case_insensitive_glob("*.pyxrd", "*.zpd")), ] import_filters = [ ("Sybilla XML files", get_case_insensitive_glob("*.xml")), ] # PROPERTIES: filename = None #: The project name name = StringProperty(default="", text="Name", visible=True, persistent=True) #: The project data (string) date = StringProperty(default="", text="Date", visible=True, persistent=True) #: The project description description = StringProperty( default=None, text="Description", visible=True, persistent=True, widget_type="text_view", ) #: The project author author = StringProperty(default="", text="Author", visible=True, persistent=True) #: Flag indicating whether this project has been changed since it was last saved. needs_saving = BoolProperty(default=True, visible=False, persistent=False) #: The layout mode this project should be displayed in layout_mode = StringChoiceProperty(default=settings.DEFAULT_LAYOUT, text="Layout mode", visible=True, persistent=True, choices=settings.DEFAULT_LAYOUTS, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The manual lower limit for the X-axis axes_xmin = FloatProperty(default=settings.AXES_MANUAL_XMIN, text="min. [°2T]", visible=True, persistent=True, minimum=0.0, widget_type="spin", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The manual upper limit for the X-axis axes_xmax = FloatProperty(default=settings.AXES_MANUAL_XMAX, text="max. [°2T]", visible=True, persistent=True, minimum=0.0, widget_type="spin", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: Whether or not to stretch the X-axis over the entire available display axes_xstretch = BoolProperty(default=settings.AXES_XSTRETCH, text="Stetch x-axis to fit window", visible=True, persistent=True, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: Flag toggling between d-spacing (when True) or 2-Theta axes (when False) axes_dspacing = BoolProperty(default=settings.AXES_DSPACING, text="Show d-spacing in x-axis", visible=True, persistent=True, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: Whether or not the y-axis should be shown axes_yvisible = BoolProperty(default=settings.AXES_YVISIBLE, text="Y-axis visible", visible=True, persistent=True, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The manual lower limit for the Y-axis (in counts) axes_ymin = FloatProperty(default=settings.AXES_MANUAL_YMIN, text="min. [counts]", visible=True, persistent=True, minimum=0.0, widget_type="spin", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The manual upper limit for the Y-axis (in counts) axes_ymax = FloatProperty(default=settings.AXES_MANUAL_YMAX, text="max. [counts]", visible=True, persistent=True, minimum=0.0, widget_type="spin", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: What type of y-axis to use: raw counts, single or multi-normalized units axes_ynormalize = IntegerChoiceProperty(default=settings.AXES_YNORMALIZE, text="Y scaling", visible=True, persistent=True, choices=settings.AXES_YNORMALIZERS, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: Whether to use automatic or manual Y limits axes_ylimit = IntegerChoiceProperty(default=settings.AXES_YLIMIT, text="Y limit", visible=True, persistent=True, choices=settings.AXES_YLIMITS, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The offset between patterns as a fraction of the maximum intensity display_plot_offset = FloatProperty(default=settings.PLOT_OFFSET, text="Pattern offset", visible=True, persistent=True, minimum=0.0, widget_type="float_entry", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The number of patterns to group ( = having no offset) display_group_by = IntegerProperty(default=settings.PATTERN_GROUP_BY, text="Group patterns by", visible=True, persistent=True, minimum=1, widget_type="spin", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The relative position (from the pattern offset) for pattern labels #: as a fraction of the patterns intensity display_label_pos = FloatProperty(default=settings.LABEL_POSITION, text="Default label position", visible=True, persistent=True, widget_type="float_entry", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: What type of scale to use for X-axis, automatic or manual axes_xlimit = IntegerChoiceProperty(default=settings.AXES_XLIMIT, text="X limit", visible=True, persistent=True, choices=settings.AXES_XLIMITS, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default angle at which marker labels are displayed display_marker_angle = FloatProperty(default=settings.MARKER_ANGLE, text="Angle", visible=True, persistent=True, widget_type="float_entry", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default offset for marker labels display_marker_top_offset = FloatProperty( default=settings.MARKER_TOP_OFFSET, text="Offset from base", visible=True, persistent=True, widget_type="float_entry", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default marker label alignment (one of settings.MARKER_ALIGNS) display_marker_align = StringChoiceProperty(default=settings.MARKER_ALIGN, text="Label alignment", visible=True, persistent=True, choices=settings.MARKER_ALIGNS, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default marker label base (one of settings.MARKER_BASES) display_marker_base = IntegerChoiceProperty(default=settings.MARKER_BASE, text="Base connection", visible=True, persistent=True, choices=settings.MARKER_BASES, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default marker label top (one of settings.MARKER_TOPS) display_marker_top = IntegerChoiceProperty(default=settings.MARKER_TOP, text="Top connection", visible=True, persistent=True, choices=settings.MARKER_TOPS, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default marker style (one of settings.MARKER_STYLES) display_marker_style = StringChoiceProperty(default=settings.MARKER_STYLE, text="Line style", visible=True, persistent=True, choices=settings.MARKER_STYLES, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default marker color display_marker_color = StringProperty(default=settings.MARKER_COLOR, text="Color", visible=True, persistent=True, widget_type="color", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default calculated profile color display_calc_color = StringProperty(default=settings.CALCULATED_COLOR, text="Calculated color", visible=True, persistent=True, widget_type="color", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default experimental profile color display_exp_color = StringProperty(default=settings.EXPERIMENTAL_COLOR, text="Experimental color", visible=True, persistent=True, widget_type="color", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default calculated profile line width display_calc_lw = IntegerProperty(default=settings.CALCULATED_LINEWIDTH, text="Calculated line width", visible=True, persistent=True, widget_type="spin", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default experimental profile line width display_exp_lw = IntegerProperty(default=settings.EXPERIMENTAL_LINEWIDTH, text="Experimental line width", visible=True, persistent=True, widget_type="spin", mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default calculated profile line style display_calc_ls = StringChoiceProperty( default=settings.CALCULATED_LINESTYLE, text="Calculated line style", visible=True, persistent=True, choices=settings.PATTERN_LINE_STYLES, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default experimental profile line style display_exp_ls = StringChoiceProperty( default=settings.EXPERIMENTAL_LINESTYLE, text="Experimental line style", visible=True, persistent=True, choices=settings.PATTERN_LINE_STYLES, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default calculated profile line style display_calc_marker = StringChoiceProperty( default=settings.CALCULATED_MARKER, text="Calculated line marker", visible=True, persistent=True, choices=settings.PATTERN_MARKERS, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The default calculated profile line style display_exp_marker = StringChoiceProperty( default=settings.EXPERIMENTAL_MARKER, text="Experimental line marker", visible=True, persistent=True, choices=settings.PATTERN_MARKERS, mix_with=(SignalMixin, ), signal_name="visuals_changed") #: The list of specimens specimens = ListProperty( default=[], text="Specimens", data_type=Specimen, visible=True, persistent=True, ) #: The list of phases phases = ListProperty(default=[], text="Phases", data_type=Phase, visible=False, persistent=True) #: The list of atom types atom_types = ListProperty(default=[], text="Atom types", data_type=AtomType, visible=False, persistent=True) #: The list of Behaviours #behaviours = ListProperty( # default=[], text="Behaviours", data_type=InSituBehaviour, # visible=False, persistent=True #) #: The list of mixtures mixtures = ListProperty(default=[], text="Mixture", data_type=Mixture, visible=False, persistent=True) # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): """ Constructor takes any of its properties as a keyword argument except for: - needs_saving In addition to the above, the constructor still supports the following deprecated keywords, mapping to a current keyword: - goniometer: the project-level goniometer, is passed on to the specimens - axes_xscale: deprecated alias for axes_xlimit - axes_yscale: deprecated alias for axes_ynormalize Any other arguments or keywords are passed to the base class. """ my_kwargs = self.pop_kwargs( kwargs, "goniometer", "data_goniometer", "data_atom_types", "data_phases", "axes_yscale", "axes_xscale", "filename", "behaviours", *[ prop.label for prop in Project.Meta.get_local_persistent_properties() ]) super(Project, self).__init__(*args, **kwargs) kwargs = my_kwargs with self.data_changed.hold(): with self.visuals_changed.hold(): self.filename = self.get_kwarg(kwargs, self.filename, "filename") self.layout_mode = self.get_kwarg(kwargs, self.layout_mode, "layout_mode") self.display_marker_align = self.get_kwarg( kwargs, self.display_marker_align, "display_marker_align") self.display_marker_color = self.get_kwarg( kwargs, self.display_marker_color, "display_marker_color") self.display_marker_base = self.get_kwarg( kwargs, self.display_marker_base, "display_marker_base") self.display_marker_top = self.get_kwarg( kwargs, self.display_marker_top, "display_marker_top") self.display_marker_top_offset = self.get_kwarg( kwargs, self.display_marker_top_offset, "display_marker_top_offset") self.display_marker_angle = self.get_kwarg( kwargs, self.display_marker_angle, "display_marker_angle") self.display_marker_style = self.get_kwarg( kwargs, self.display_marker_style, "display_marker_style") self.display_calc_color = self.get_kwarg( kwargs, self.display_calc_color, "display_calc_color") self.display_exp_color = self.get_kwarg( kwargs, self.display_exp_color, "display_exp_color") self.display_calc_lw = self.get_kwarg(kwargs, self.display_calc_lw, "display_calc_lw") self.display_exp_lw = self.get_kwarg(kwargs, self.display_exp_lw, "display_exp_lw") self.display_calc_ls = self.get_kwarg(kwargs, self.display_calc_ls, "display_calc_ls") self.display_exp_ls = self.get_kwarg(kwargs, self.display_exp_ls, "display_exp_ls") self.display_calc_marker = self.get_kwarg( kwargs, self.display_calc_marker, "display_calc_marker") self.display_exp_marker = self.get_kwarg( kwargs, self.display_exp_marker, "display_exp_marker") self.display_plot_offset = self.get_kwarg( kwargs, self.display_plot_offset, "display_plot_offset") self.display_group_by = self.get_kwarg(kwargs, self.display_group_by, "display_group_by") self.display_label_pos = self.get_kwarg( kwargs, self.display_label_pos, "display_label_pos") self.axes_xlimit = self.get_kwarg(kwargs, self.axes_xlimit, "axes_xlimit", "axes_xscale") self.axes_xmin = self.get_kwarg(kwargs, self.axes_xmin, "axes_xmin") self.axes_xmax = self.get_kwarg(kwargs, self.axes_xmax, "axes_xmax") self.axes_xstretch = self.get_kwarg(kwargs, self.axes_xstretch, "axes_xstretch") self.axes_ylimit = self.get_kwarg(kwargs, self.axes_ylimit, "axes_ylimit") self.axes_ynormalize = self.get_kwarg(kwargs, self.axes_ynormalize, "axes_ynormalize", "axes_yscale") self.axes_yvisible = self.get_kwarg(kwargs, self.axes_yvisible, "axes_yvisible") self.axes_ymin = self.get_kwarg(kwargs, self.axes_ymin, "axes_ymin") self.axes_ymax = self.get_kwarg(kwargs, self.axes_ymax, "axes_ymax") goniometer = None goniometer_kwargs = self.get_kwarg(kwargs, None, "goniometer", "data_goniometer") if goniometer_kwargs: goniometer = self.parse_init_arg(goniometer_kwargs, None, child=True) # Set up and observe atom types: self.atom_types = self.get_list(kwargs, [], "atom_types", "data_atom_types", parent=self) self._atom_types_observer = ListObserver( self.on_atom_type_inserted, self.on_atom_type_removed, prop_name="atom_types", model=self) # Resolve json references & observe phases self.phases = self.get_list(kwargs, [], "phases", "data_phases", parent=self) for phase in self.phases: phase.resolve_json_references() self.observe_model(phase) self._phases_observer = ListObserver(self.on_phase_inserted, self.on_phase_removed, prop_name="phases", model=self) # Set goniometer if required & observe specimens self.specimens = self.get_list(kwargs, [], "specimens", "data_specimens", parent=self) for specimen in self.specimens: if goniometer: specimen.goniometer = goniometer self.observe_model(specimen) self._specimens_observer = ListObserver( self.on_specimen_inserted, self.on_specimen_removed, prop_name="specimens", model=self) # Observe behaviours: #self.behaviours = self.get_list(kwargs, [], "behaviours", parent=self) #for behaviour in self.behaviours: # self.observe_model(behaviour) #self._behaviours_observer = ListObserver( # self.on_behaviour_inserted, # self.on_behaviour_removed, # prop_name="behaviours", # model=self #) # Observe mixtures: self.mixtures = self.get_list(kwargs, [], "mixtures", "data_mixtures", parent=self) for mixture in self.mixtures: self.observe_model(mixture) self._mixtures_observer = ListObserver( self.on_mixture_inserted, self.on_mixture_removed, prop_name="mixtures", model=self) self.name = str( self.get_kwarg(kwargs, "Project name", "name", "data_name")) self.date = str( self.get_kwarg(kwargs, time.strftime("%d/%m/%Y"), "date", "data_date")) self.description = str( self.get_kwarg(kwargs, "Project description", "description", "data_description")) self.author = str( self.get_kwarg(kwargs, "Project author", "author", "data_author")) load_default_data = self.get_kwarg(kwargs, True, "load_default_data") if load_default_data and self.layout_mode != 1 and \ len(self.atom_types) == 0: self.load_default_data() self.needs_saving = True pass # end with visuals_changed pass # end with data_changed def load_default_data(self): for atom_type in AtomType.get_from_csv( settings.DATA_REG.get_file_path("ATOM_SCAT_FACTORS")): self.atom_types.append(atom_type) # ------------------------------------------------------------ # Notifications of observable properties # ------------------------------------------------------------ def on_phase_inserted(self, item): # Set parent on the new phase: if item.parent != self: item.parent = self item.resolve_json_references() def on_phase_removed(self, item): with self.data_changed.hold_and_emit(): # Clear parent: item.parent = None # Clear links with other phases: if getattr(item, "based_on", None) is not None: item.based_on = None for phase in self.phases: if getattr(phase, "based_on", None) == item: phase.based_on = None # Remove phase from mixtures: for mixture in self.mixtures: mixture.unset_phase(item) def on_atom_type_inserted(self, item, *data): if item.parent != self: item.parent = self # We do not observe AtomType's directly, if they change, # Atoms containing them will be notified, and that event should bubble # up to the project level. def on_atom_type_removed(self, item, *data): item.parent = None # We do not emit a signal for AtomType's, if it was part of # an Atom, the Atom will be notified, and the event should bubble # up to the project level def on_specimen_inserted(self, item): # Set parent and observe the new specimen (visuals changed signals): if item.parent != self: item.parent = self self.observe_model(item) def on_specimen_removed(self, item): with self.data_changed.hold_and_emit(): # Clear parent & stop observing: item.parent = None self.relieve_model(item) # Remove specimen from mixtures: for mixture in self.mixtures: mixture.unset_specimen(item) def on_mixture_inserted(self, item): # Set parent and observe the new mixture: if item.parent != self: item.parent = self self.observe_model(item) def on_mixture_removed(self, item): with self.data_changed.hold_and_emit(): # Clear parent & stop observing: item.parent = None self.relieve_model(item) def on_behaviour_inserted(self, item): # Set parent and observe the new mixture: if item.parent != self: item.parent = self self.observe_model(item) def on_behaviour_removed(self, item): with self.data_changed.hold_and_emit(): # Clear parent & stop observing: item.parent = None self.relieve_model(item) @DataModel.observe("data_changed", signal=True) def notify_data_changed(self, model, prop_name, info): self.needs_saving = True if isinstance(model, Mixture): self.data_changed.emit() @DataModel.observe("visuals_changed", signal=True) def notify_visuals_changed(self, model, prop_name, info): self.needs_saving = True self.visuals_changed.emit() # propagate signal # ------------------------------------------------------------ # Input/Output stuff # ------------------------------------------------------------ @classmethod def from_json(type, **kwargs): # @ReservedAssignment project = type(**kwargs) project.needs_saving = False # don't mark this when just loaded return project def to_json_multi_part(self): to_json = self.to_json() properties = to_json["properties"] for name in ("phases", "specimens", "atom_types", "mixtures"): #"behaviours" yield (name, properties.pop(name)) properties[name] = "file://%s" % name yield ("content", to_json) yield ("version", __version__) @staticmethod def create_from_sybilla_xml(filename, **kwargs): from pyxrd.project.importing import create_project_from_sybilla_xml return create_project_from_sybilla_xml(filename, **kwargs) # ------------------------------------------------------------ # Draggable mix-in hook: # ------------------------------------------------------------ def on_label_dragged(self, delta_y, button=1): if button == 1: self.display_label_pos += delta_y pass # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def get_scale_factor(self, specimen=None): """ Get the factor with which to scale raw data and the scaled offset :rtype: tuple containing the scale factor and the (scaled) offset """ if self.axes_ynormalize == 0 or (self.axes_ynormalize == 1 and specimen is None): return (1.0 / (self.get_max_display_y() or 1.0), 1.0) elif self.axes_ynormalize == 1: return (1.0 / (specimen.get_max_display_y or 1.0), 1.0) elif self.axes_ynormalize == 2: return (1.0, self.get_max_display_y()) else: raise ValueError( "Wrong value for 'axes_ysnormalize' in %s: is `%d`; should be 0, 1 or 2" % (self, self.axes_ynormalize)) def get_max_display_y(self): max_display_y = 0 if self.parent is not None: for specimen in self.parent.current_specimens: max_display_y = max(specimen.max_display_y, max_display_y) return max_display_y @contextmanager def hold_child_signals(self): logger.info("Holding back all project child object signals") with self.hold_mixtures_needs_update(): with self.hold_mixtures_data_changed(): with self.hold_phases_data_changed(): with self.hold_specimens_data_changed(): with self.hold_atom_types_data_changed(): yield @contextmanager def hold_mixtures_needs_update(self): logger.info("Holding back all 'needs_update' signals from Mixtures") with EventContextManager( *[mixture.needs_update.hold() for mixture in self.mixtures]): yield @contextmanager def hold_mixtures_data_changed(self): logger.info("Holding back all 'data_changed' signals from Mixtures") with EventContextManager( *[mixture.data_changed.hold() for mixture in self.mixtures]): yield @contextmanager def hold_phases_data_changed(self): logger.info("Holding back all 'data_changed' signals from Phases") with EventContextManager( *[phase.data_changed.hold() for phase in self.phases]): yield @contextmanager def hold_atom_types_data_changed(self): logger.info("Holding back all 'data_changed' signals from AtomTypes") with EventContextManager( * [atom_type.data_changed.hold() for atom_type in self.atom_types]): yield @contextmanager def hold_specimens_data_changed(self): logger.info("Holding back all 'data_changed' signals from Specimens") with EventContextManager( *[specimen.data_changed.hold() for specimen in self.specimens]): yield def update_all_mixtures(self): """ Forces all mixtures in this project to update. If they have auto optimization enabled, this will also optimize them. """ for mixture in self.mixtures: with self.data_changed.ignore(): mixture.update() def get_mixtures_by_name(self, mixture_name): """ Convenience method that returns all the mixtures who's name match the passed name as a list. """ return [ mixture for mixture in self.mixtures if (mixture.name == mixture_name) ] # ------------------------------------------------------------ # Specimen list related # ------------------------------------------------------------ def move_specimen_up(self, specimen): """ Move the passed :class:`~pyxrd.specimen.models.Specimen` up one slot. Will raise and IndexError if the passed specimen is not in this project. """ index = self.specimens.index(specimen) self.specimens.insert(min(index + 1, len(self.specimens)), self.specimens.pop(index)) def move_specimen_down(self, specimen): """ Move the passed :class:`~pyxrd.specimen.models.Specimen` down one slot Will raise and IndexError if the passed specimen is not in this project. """ index = self.specimens.index(specimen) self.specimens.insert(max(index - 1, 0), self.specimens.pop(index)) pass # ------------------------------------------------------------ # Phases list related # ------------------------------------------------------------ def load_phases(self, filename, parser, insert_index=0): """ Loads all :class:`~pyxrd.phase.models.Phase` objects from the file 'filename'. An optional index can be given where the phases need to be inserted at. """ # make sure we have no duplicate UUID's insert_index = not_none(insert_index, 0) type(Project).object_pool.change_all_uuids() for phase in parser.parse(filename): phase.parent = self self.phases.insert(insert_index, phase) insert_index += 1 # ------------------------------------------------------------ # AtomType's list related # ------------------------------------------------------------ def load_atom_types(self, filename, parser): """ Loads all :class:`~pyxrd.atoms.models.AtomType` objects from the file specified by *filename*. """ # make sure we have no duplicate UUID's type(Project).object_pool.change_all_uuids() for atom_type in parser.parse(filename): atom_type.parent = self self.atom_types.append(atom_type) pass # end of class