class Mixture(DataModel, Storable): """ The base model for optimization and refinement of calculated data and experimental data. This is the main model you want to interact with, lower-level classes' functionality (mainly :class:`~pyxrd.mixture.models.optimizers.Optimizer` and :class:`~pyxrd.mixture.models.refiner.Refiner`) are integrated into this class. The Mixture is responsible for managing the phases and specimens lists and combination-matrix and for delegating any optimization, calculation or refinement calls to the appropriate helper class. """ # MODEL INTEL: class Meta(DataModel.Meta): properties = [ # TODO add labels PropIntel(name="name", label="Name", data_type=unicode, storable=True, has_widget=True, is_column=True), PropIntel(name="refinables", label="", data_type=object, widget_type="object_tree_view", class_type=RefinableWrapper), PropIntel(name="refine_options", label="", data_type=dict, storable=True, stor_name="all_refine_options"), PropIntel(name="refine_method", label="", data_type=int, storable=True), PropIntel(name="auto_run", label="", data_type=bool, is_column=True, storable=True, has_widget=True), PropIntel(name="auto_bg", label="", data_type=bool, is_column=True, storable=True, has_widget=True), PropIntel(name="needs_update", label="", data_type=object, storable=False), # Signal used to indicate the mixture needs an update PropIntel(name="needs_reset", label="", data_type=object, storable=False,), # Signal used to indicate the mixture matrix needs to be re-built... ] store_id = "Mixture" _data_object = None @property def data_object(self): self._data_object.specimens = [None] * len(self.specimens) self._data_object.auto_bg = self.auto_bg for i, specimen in enumerate(self.specimens): if specimen is not None: data_object = specimen.data_object data_object.phases = [None] * len(self.phases) for j, phase in enumerate(self.phase_matrix[i, ...].flatten()): data_object.phases[j] = phase.data_object if phase is not None else None self._data_object.specimens[i] = data_object else: self._data_object.specimens[i] = None return self._data_object project = property(DataModel.parent.fget, DataModel.parent.fset) # SIGNALS: #: Signal, emitted when the # of phases or specimens changes needs_reset = None #: Signal, emitted when the patterns of the specimens need an update needs_update = None # INTERNALS: _name = "" @property def name(self): """ The name of this mixture """ return self._name @name.setter def name(self, value): self._name = value self.visuals_changed.emit() #: Flag, True if the mixture will automatically adjust phase fractions and scales auto_run = False #: Flag, True if the mixture is allowed to also update the background level auto_bg = True #: The tree of refinable properties @property def refinables(self): return self.refiner.refinables @property def refine_method(self): """ An integer describing which method to use for the refinement (see mixture.models.methods.get_all_refine_methods) """ return self.refiner.refine_method #: A dict containing the current refinement options @property def refine_options(self): return self.refiner.refine_options #: A dict containing all refinement options @property def all_refine_options(self): return self.refiner.all_refine_options # Lists and matrices: #: A 2D numpy object array containing the combination matrix phase_matrix = None #: The list of specimen objects specimens = None #: The list of phase names phases = None @property def scales(self): """ A list of floats containing the absolute scales for the calculated patterns """ return self._data_object.scales @scales.setter def scales(self, value): self._data_object.scales = value @property def bgshifts(self): """ A list of background shifts for the calculated patterns """ return self._data_object.bgshifts @bgshifts.setter def bgshifts(self, value): self._data_object.bgshifts = value @property def fractions(self): """ A list of phase fractions for this mixture """ return self._data_object.fractions @fractions.setter def fractions(self, value): self._data_object.fractions = value # ------------------------------------------------------------ # Initialization and other internals # ------------------------------------------------------------ def __init__(self, *args, **kwargs): """ Constructor takes any of its properties as a keyword argument. It also support two UUID list keyword arguments: - phase_uuids: a list of UUID's for the phases in the mixture - specimen_uuids: a list of UUID's for the specimens in the mixture These should be *instead* of the phases and specimens keywords. In addition to the above, the constructor still supports the following deprecated keywords, mapping to a current keyword: - phase_indeces: a list of project indices for the phases in the mixture - specimen_indeces: a list of project indices for the specimens in the mixture Any other arguments or keywords are passed to the base class. """ my_kwargs = self.pop_kwargs(kwargs, "data_name", "phase_uuids", "phase_indeces", "specimen_uuids", "specimen_indeces", "data_phases", "data_scales", "data_bgshifts", "data_fractions", "data_refine_method", "fractions", "bgshifts", "scales", "phases", *[names[0] for names in type(self).Meta.get_local_storable_properties()] ) super(Mixture, self).__init__(*args, **kwargs) kwargs = my_kwargs with self.data_changed.hold(): self._data_object = MixtureData() self.needs_reset = Signal() self.needs_update = HoldableSignal() self.name = self.get_kwarg(kwargs, "New Mixture", "name", "data_name") self.auto_run = self.get_kwarg(kwargs, False, "auto_run") self.auto_bg = self.get_kwarg(kwargs, True, "auto_bg") # 2D matrix, rows match specimens, columns match mixture 'phases'; contains the actual phase objects phase_uuids = self.get_kwarg(kwargs, None, "phase_uuids") phase_indeces = self.get_kwarg(kwargs, None, "phase_indeces") if phase_uuids is not None: self.phase_matrix = np.array([[type(type(self)).object_pool.get_object(uuid) if uuid else None for uuid in row] for row in phase_uuids], dtype=np.object_) elif phase_indeces and self.parent is not None: warn("The use of object indices is deprecated since version 0.4. Please switch to using object UUIDs.", DeprecationWarning) self.phase_matrix = np.array([[self.parent.phases.get_user_data_from_index(index) if index != -1 else None for index in row] for row in phase_indeces], dtype=np.object_) else: self.phase_matrix = np.empty(shape=(0, 0), dtype=np.object_) # list with actual specimens, indexes match with rows in phase_matrix specimen_uuids = self.get_kwarg(kwargs, None, "specimen_uuids") specimen_indeces = self.get_kwarg(kwargs, None, "specimen_indeces") if specimen_uuids: self.specimens = [type(type(self)).object_pool.get_object(uuid) if uuid else None for uuid in specimen_uuids] elif specimen_indeces and self.parent is not None: warn("The use of object indices is deprecated since version 0.4. Please switch to using object UUIDs.", DeprecationWarning) self.specimens = [self.parent.specimens.get_user_data_from_index(index) if index != -1 else None for index in specimen_indeces] else: self.specimens = list() # list with mixture phase names, indexes match with cols in phase_matrix self.phases = self.get_kwarg(kwargs, list(), "phases", "data_phases") # list with scale values, indexes match with rows in phase_matrix (= specimens) self.scales = np.asarray(self.get_kwarg(kwargs, [1.0] * len(self.specimens), "scales", "data_scales")) # list with specimen background shift values, indexes match with rows in phase_matrix (=specimens) self.bgshifts = np.asarray(self.get_kwarg(kwargs, [0.0] * len(self.specimens), "bgshifts", "data_bgshifts")) # list with phase fractions, indexes match with cols in phase_matrix (=phases) self.fractions = np.asarray(self.get_kwarg(kwargs, [0.0] * len(self.phases), "fractions", "data_fractions")) # sanity check: n, m = self.phase_matrix.shape if self.phase_matrix.ndim == 2 else (0, 0) if len(self.scales) != n or len(self.specimens) != n or len(self.bgshifts) != n: raise IndexError, "Shape mismatch: scales (%d), background shifts (%d) or specimens (%d) list lengths do not match with row count (%d) of phase matrix" % (len(self.scales), len(self.specimens), len(self.bgshifts), n) if len(self.phases) != m or len(self.fractions) != m: raise IndexError, "Shape mismatch: fractions or phases lists do not match with column count of phase matrix" self._observe_specimens() self._observe_phases() self.optimizer = Optimizer(parent=self) self.refiner = Refiner( refine_method=self.get_kwarg(kwargs, 0, "refine_method", "data_refine_method"), refine_options=self.get_kwarg(kwargs, dict(), "refine_options"), parent=self) self.update() self.observe_model(self) pass # end hold data_changed # ------------------------------------------------------------ # Notifications of observable properties # ------------------------------------------------------------ @DataModel.observe("removed", signal=True) def notify_removed(self, model, prop_name, info): if model == self: self._relieve_phases() self._relieve_specimens() @DataModel.observe("needs_update", signal=True) def notify_needs_update(self, model, prop_name, info): with self.data_changed.hold(): self.update() @DataModel.observe("data_changed", signal=True) def notify_data_changed(self, model, prop_name, info): if not model == self and not ( info.arg == "based_on" and model.based_on is not None and model.based_on in self.phase_matrix): self.needs_update.emit() @DataModel.observe("visuals_changed", signal=True) def notify_visuals_changed(self, model, prop_name, info): if isinstance(model, Phase) and \ not (info.arg == "based_on" and model.based_on is not None and model.based_on in self.phase_matrix): for i, specimen in enumerate(self.specimens): if specimen is not None: specimen.update_visuals(self.phase_matrix[i, :]) # ------------------------------------------------------------ # Input/Output stuff # ------------------------------------------------------------ def json_properties(self): self.refiner.update_refinement_treestore() retval = Storable.json_properties(self) retval["phase_uuids"] = [[item.uuid if item else "" for item in row] for row in map(list, self.phase_matrix)] retval["specimen_uuids"] = [specimen.uuid if specimen else "" for specimen in self.specimens] retval["phases"] = self.phases retval["fractions"] = self.fractions.tolist() retval["bgshifts"] = self.bgshifts.tolist() retval["scales"] = self.scales.tolist() return retval @staticmethod def from_json(**kwargs): # Remove this deprecated kwarg: if "refinables" in kwargs: del kwargs["refinables"] return Mixture(**kwargs) # ------------------------------------------------------------ # Methods & Functions # ------------------------------------------------------------ def unset_phase(self, phase): """ Clears a phase slot in the phase matrix """ with self.needs_update.hold_and_emit(): with self.data_changed.hold(): shape = self.phase_matrix.shape with self._relieve_and_observe_phases(): for i in range(shape[0]): for j in range(shape[1]): if self.phase_matrix[i, j] == phase: self.phase_matrix[i, j] = None self.refiner.update_refinement_treestore() def unset_specimen(self, specimen): """ Clears a specimen slot in the specimen list """ with self.needs_update.hold_and_emit(): with self.data_changed.hold(): with self._relieve_and_observe_specimens(): for i, spec in enumerate(self.specimens): if spec == specimen: self.specimens[i] = None def get_phase(self, specimen_slot, phase_slot): """Returns the phase at the given slot positions or None if not set""" return self.phase_matrix[specimen_slot, phase_slot] def set_phase(self, specimen_slot, phase_slot, phase): """Sets the phase at the given slot positions""" if self.parent is not None: #no parent = no valid phases with self.needs_update.hold_and_emit(): with self.data_changed.hold(): with self._relieve_and_observe_phases(): if phase is not None and not phase in self.parent.phases: raise RuntimeError, "Cannot add a phase to a Mixture which is not inside the project!" self.phase_matrix[specimen_slot, phase_slot] = phase self.refiner.update_refinement_treestore() def get_specimen(self, specimen_slot): """Returns the specimen at the given slot position or None if not set""" return self.specimens[specimen_slot] def set_specimen(self, specimen_slot, specimen): """Sets the specimen at the given slot position""" if self.parent is not None: #no parent = no valid specimens with self.needs_update.hold_and_emit(): with self.data_changed.hold(): with self._relieve_and_observe_specimens(): if specimen is not None and not specimen in self.parent.specimens: raise RuntimeError, "Cannot add a specimen to a Mixture which is not inside the project!" self.specimens[specimen_slot] = specimen @contextmanager def _relieve_and_observe_specimens(self): self._relieve_specimens() yield self._observe_specimens() def _observe_specimens(self): """ Starts observing specimens in the specimens list""" for specimen in self.specimens: if specimen is not None: self.observe_model(specimen) def _relieve_specimens(self): """ Relieves specimens observer calls """ for specimen in self.specimens: if specimen is not None: self.relieve_model(specimen) @contextmanager def _relieve_and_observe_phases(self): self._relieve_phases() yield self._observe_phases() def _observe_phases(self): """ Starts observing phases in the phase matrix""" for phase in self.phase_matrix.flat: if phase is not None: self.observe_model(phase) def _relieve_phases(self): """ Relieves phase observer calls """ for phase in self.phase_matrix.flat: if phase is not None: self.relieve_model(phase) def add_phase_slot(self, phase_name, fraction): """ Adds a new phase column to the phase matrix """ with self.needs_update.hold_and_emit(): with self.data_changed.hold(): self.phases.append(phase_name) self.fractions = np.append(self.fractions, fraction) n, m = self.phase_matrix.shape if self.phase_matrix.ndim == 2 else (0, 0) if self.phase_matrix.size == 0: self.phase_matrix = np.resize(self.phase_matrix.copy(), (n, m + 1)) self.phase_matrix[:] = None else: self.phase_matrix = np.concatenate([self.phase_matrix.copy(), [[None]] * n ], axis=1) self.phase_matrix[:, m] = None self.refiner.update_refinement_treestore() return m def del_phase_slot(self, phase_slot): """ Deletes a phase column using its index """ with self.needs_update.hold_and_emit(): with self.data_changed.hold(): with self._relieve_and_observe_phases(): # Remove the corresponding phase name, fraction & references: del self.phases[phase_slot] self.fractions = np.delete(self.fractions, phase_slot) self.phase_matrix = np.delete(self.phase_matrix, phase_slot, axis=1) # Update our refinement tree store to reflect current state self.refiner.update_refinement_treestore() # Inform any interested party they need to update their representation self.needs_reset.emit() def del_phase_slot_by_name(self, phase_name): """ Deletes a phase slot using its name """ self.del_phase_slot(self.phases.index(phase_name)) def add_specimen_slot(self, specimen, scale, bgs): """ Adds a new specimen to the phase matrix (a row) and specimen list """ with self.needs_update.hold_and_emit(): with self.data_changed.hold(): self.specimens.append(None) self.scales = np.append(self.scales, scale) self.bgshifts = np.append(self.bgshifts, bgs) n, m = self.phase_matrix.shape if self.phase_matrix.ndim == 2 else (0, 0) if self.phase_matrix.size == 0: self.phase_matrix = np.resize(self.phase_matrix.copy(), (n + 1, m)) self.phase_matrix[:] = None else: self.phase_matrix = np.concatenate([self.phase_matrix.copy(), [[None] * m] ], axis=0) self.phase_matrix[n, :] = None if specimen is not None: self.set_specimen(n, specimen) return n def del_specimen_slot(self, specimen_slot): """ Deletes a specimen slot using its slot index """ with self.needs_update.hold_and_emit(): with self.data_changed.hold(): # Remove the corresponding specimen name, scale, bg-shift & phases: with self._relieve_and_observe_specimens(): del self.specimens[specimen_slot] self.scales = np.delete(self.scales, specimen_slot) self.bgshifts = np.delete(self.bgshifts, specimen_slot) self.phase_matrix = np.delete(self.phase_matrix, specimen_slot, axis=0) # Update our refinement tree store to reflect current state self.refiner.update_refinement_treestore() # Inform any interested party they need to update their representation self.needs_reset.emit() def del_specimen_slot_by_object(self, specimen): """ Deletes a specimen slot using the actual object """ try: self.del_specimen_slot(self.specimens.index(specimen)) except ValueError: logger.exception("Caught a ValueError when deleting a specimen from mixture '%s'" % self.name) # ------------------------------------------------------------ # Refinement stuff: # ------------------------------------------------------------ def set_data_object(self, mixture, calculate=False): """ Sets the fractions, scales and bgshifts of this mixture. """ if mixture is not None: with self.needs_update.ignore(): with self.data_changed.hold_and_emit(): self.fractions[:] = list(mixture.fractions) self.scales[:] = list(mixture.scales) self.bgshifts[:] = list(mixture.bgshifts) if calculate: # (re-)calculate if requested: mixture = self.optimizer.calculate(mixture) for i, (specimen_data, specimen) in enumerate(izip(mixture.specimens, self.specimens)): if specimen is not None: with specimen.data_changed.ignore(): specimen.update_pattern( specimen_data.total_intensity, specimen_data.phase_intensities * self.fractions[:, np.newaxis] * self.scales[i], self.phase_matrix[i, :] ) def optimize(self): """ Optimize the current solution (fractions, scales, bg shifts & calculate phase intensities) """ with self.needs_update.ignore(): with self.data_changed.hold(): # no need to re-calculate, is already done by the optimization self.set_data_object(self.optimizer.optimize()) def apply_current_data_object(self): """ Recalculates the intensities using the current fractions, scales and bg shifts without optimization """ with self.needs_update.ignore(): with self.data_changed.hold(): self.set_data_object(self.data_object, calculate=True) # @print_timing def update(self): """ Optimizes or re-applies the current mixture 'solution'. Effectively re-calculates the entire patterns. """ with self.needs_update.ignore(): with self.data_changed.hold(): if self.auto_run: self.optimize() else: self.apply_current_data_object() # ------------------------------------------------------------ # Various other things: # ------------------------------------------------------------ def get_composition_matrix(self): """ Returns a matrix containing the oxide composition for each specimen in this mixture. It uses the COMPOSITION_CONV file for this purpose to convert element weights into their oxide weight equivalent. """ # create an atom nr -> (atom name, conversion) mapping # this is used to transform most of the elements into their oxides atom_conv = OrderedDict() with open(settings.DATA_REG.get_file_path("COMPOSITION_CONV"), 'r') as f: reader = csv.reader(f) reader.next() # skip header for row in reader: nr, name, fact = row atom_conv[int(nr)] = (name, float(fact)) comps = list() for i, row in enumerate(self.phase_matrix): comp = dict() for j, phase in enumerate(row): phase_fract = self.fractions[j] for k, component in enumerate(phase.components): comp_fract = phase.probabilities.mW[k] * phase_fract for atom in chain(component.layer_atoms, component.interlayer_atoms): nr = atom.atom_type.atom_nr if nr in atom_conv: wt = atom.pn * atom.atom_type.weight * comp_fract * atom_conv[nr][1] comp[nr] = comp.get(nr, 0.0) + wt comps.append(comp) final_comps = np.zeros(shape=(len(atom_conv) + 1, len(comps) + 1), dtype='a15') final_comps[0, 0] = " "*8 for j, comp in enumerate(comps): fact = 100.0 / sum(comp.values()) for i, (nr, (oxide_name, conv)) in enumerate(atom_conv.iteritems()): wt = comp.get(nr, 0.0) * fact # set relevant cells: if i == 0: final_comps[i, j + 1] = self.specimens[j].name.ljust(15)[:15] if j == 0: final_comps[i + 1, j] = ("%s " % oxide_name).rjust(8)[:8] final_comps[i + 1, j + 1] = ("%.1f" % wt).ljust(15)[:15] return final_comps pass # end of class
def __init__(self, *args, **kwargs): """ Constructor takes any of its properties as a keyword argument. It also support two UUID list keyword arguments: - phase_uuids: a list of UUID's for the phases in the mixture - specimen_uuids: a list of UUID's for the specimens in the mixture These should be *instead* of the phases and specimens keywords. In addition to the above, the constructor still supports the following deprecated keywords, mapping to a current keyword: - phase_indeces: a list of project indices for the phases in the mixture - specimen_indeces: a list of project indices for the specimens in the mixture Any other arguments or keywords are passed to the base class. """ my_kwargs = self.pop_kwargs(kwargs, "data_name", "phase_uuids", "phase_indeces", "specimen_uuids", "specimen_indeces", "data_phases", "data_scales", "data_bgshifts", "data_fractions", "data_refine_method", "fractions", "bgshifts", "scales", "phases", *[names[0] for names in type(self).Meta.get_local_storable_properties()] ) super(Mixture, self).__init__(*args, **kwargs) kwargs = my_kwargs with self.data_changed.hold(): self._data_object = MixtureData() self.needs_reset = Signal() self.needs_update = HoldableSignal() self.name = self.get_kwarg(kwargs, "New Mixture", "name", "data_name") self.auto_run = self.get_kwarg(kwargs, False, "auto_run") self.auto_bg = self.get_kwarg(kwargs, True, "auto_bg") # 2D matrix, rows match specimens, columns match mixture 'phases'; contains the actual phase objects phase_uuids = self.get_kwarg(kwargs, None, "phase_uuids") phase_indeces = self.get_kwarg(kwargs, None, "phase_indeces") if phase_uuids is not None: self.phase_matrix = np.array([[type(type(self)).object_pool.get_object(uuid) if uuid else None for uuid in row] for row in phase_uuids], dtype=np.object_) elif phase_indeces and self.parent is not None: warn("The use of object indices is deprecated since version 0.4. Please switch to using object UUIDs.", DeprecationWarning) self.phase_matrix = np.array([[self.parent.phases.get_user_data_from_index(index) if index != -1 else None for index in row] for row in phase_indeces], dtype=np.object_) else: self.phase_matrix = np.empty(shape=(0, 0), dtype=np.object_) # list with actual specimens, indexes match with rows in phase_matrix specimen_uuids = self.get_kwarg(kwargs, None, "specimen_uuids") specimen_indeces = self.get_kwarg(kwargs, None, "specimen_indeces") if specimen_uuids: self.specimens = [type(type(self)).object_pool.get_object(uuid) if uuid else None for uuid in specimen_uuids] elif specimen_indeces and self.parent is not None: warn("The use of object indices is deprecated since version 0.4. Please switch to using object UUIDs.", DeprecationWarning) self.specimens = [self.parent.specimens.get_user_data_from_index(index) if index != -1 else None for index in specimen_indeces] else: self.specimens = list() # list with mixture phase names, indexes match with cols in phase_matrix self.phases = self.get_kwarg(kwargs, list(), "phases", "data_phases") # list with scale values, indexes match with rows in phase_matrix (= specimens) self.scales = np.asarray(self.get_kwarg(kwargs, [1.0] * len(self.specimens), "scales", "data_scales")) # list with specimen background shift values, indexes match with rows in phase_matrix (=specimens) self.bgshifts = np.asarray(self.get_kwarg(kwargs, [0.0] * len(self.specimens), "bgshifts", "data_bgshifts")) # list with phase fractions, indexes match with cols in phase_matrix (=phases) self.fractions = np.asarray(self.get_kwarg(kwargs, [0.0] * len(self.phases), "fractions", "data_fractions")) # sanity check: n, m = self.phase_matrix.shape if self.phase_matrix.ndim == 2 else (0, 0) if len(self.scales) != n or len(self.specimens) != n or len(self.bgshifts) != n: raise IndexError, "Shape mismatch: scales (%d), background shifts (%d) or specimens (%d) list lengths do not match with row count (%d) of phase matrix" % (len(self.scales), len(self.specimens), len(self.bgshifts), n) if len(self.phases) != m or len(self.fractions) != m: raise IndexError, "Shape mismatch: fractions or phases lists do not match with column count of phase matrix" self._observe_specimens() self._observe_phases() self.optimizer = Optimizer(parent=self) self.refiner = Refiner( refine_method=self.get_kwarg(kwargs, 0, "refine_method", "data_refine_method"), refine_options=self.get_kwarg(kwargs, dict(), "refine_options"), parent=self) self.update() self.observe_model(self) pass # end hold data_changed