def driving_force_to_hyperplane( target_hyperplane_chempots: np.ndarray, phase_region: PhaseRegion, vertex: RegionVertex, parameters: np.ndarray, approximate_equilibrium: bool = False) -> float: """Calculate the integrated driving force between the current hyperplane and target hyperplane. """ species = phase_region.species models = phase_region.models current_phase = vertex.phase_name cond_dict = {**phase_region.potential_conds, **vertex.comp_conds} str_statevar_dict = OrderedDict([ (str(key), cond_dict[key]) for key in sorted(phase_region.potential_conds.keys(), key=str) ]) phase_points = vertex.points phase_records = vertex.phase_records update_phase_record_parameters(phase_records, parameters) if phase_points is None: # We don't have the phase composition here, so we estimate the driving force. # Can happen if one of the composition conditions is unknown or if the phase is # stoichiometric and the user did not specify a valid phase composition. single_eqdata = calculate_(species, [current_phase], str_statevar_dict, models, phase_records, pdens=50) df = np.multiply(target_hyperplane_chempots, single_eqdata.X).sum(axis=-1) - single_eqdata.GM driving_force = float(df.max()) elif vertex.is_disordered: # Construct disordered sublattice configuration from composition dict # Compute energy # Compute residual driving force # TODO: Check that it actually makes sense to declare this phase 'disordered' num_dof = sum( [len(subl) for subl in models[current_phase].constituents]) desired_sitefracs = np.ones(num_dof, dtype=np.float_) dof_idx = 0 for subl in models[current_phase].constituents: dof = sorted(subl, key=str) num_subl_dof = len(subl) if v.Species("VA") in dof: if num_subl_dof == 1: _log.debug( 'Cannot predict the site fraction of vacancies in the disordered configuration %s of %s. Returning driving force of zero.', subl, current_phase) return 0 else: sitefracs_to_add = [1.0] else: sitefracs_to_add = np.array( [cond_dict.get(v.X(d)) for d in dof], dtype=np.float_) # Fix composition of dependent component sitefracs_to_add[np.isnan( sitefracs_to_add)] = 1 - np.nansum(sitefracs_to_add) desired_sitefracs[dof_idx:dof_idx + num_subl_dof] = sitefracs_to_add dof_idx += num_subl_dof single_eqdata = calculate_(species, [current_phase], str_statevar_dict, models, phase_records, points=np.asarray([desired_sitefracs])) driving_force = np.multiply( target_hyperplane_chempots, single_eqdata.X).sum(axis=-1) - single_eqdata.GM driving_force = float(np.squeeze(driving_force)) else: # Extract energies from single-phase calculations grid = calculate_(species, [current_phase], str_statevar_dict, models, phase_records, points=phase_points, pdens=50, fake_points=True) # TODO: consider enabling approximate for this? converged, energy = constrained_equilibrium(phase_records, cond_dict, grid) if not converged: _log.debug( 'Calculation failure: constrained equilibrium not converged for %s, conditions: %s, parameters %s', current_phase, cond_dict, parameters) return np.inf driving_force = float( np.dot(target_hyperplane_chempots, vertex.composition) - float(energy)) return driving_force
def calc_prop_differences( eqpropdata: EqPropData, parameters: np.ndarray, approximate_equilibrium: Optional[bool] = False, ) -> Tuple[np.ndarray, np.ndarray]: """ Calculate differences between the expected and calculated values for a property Parameters ---------- eqpropdata : EqPropData Data corresponding to equilibrium calculations for a single datasets. parameters : np.ndarray Array of parameters to fit. Must be sorted in the same symbol sorted order used to create the PhaseRecords. approximate_equilibrium : Optional[bool] Whether or not to use an approximate version of equilibrium that does not refine the solution and uses ``starting_point`` instead. Returns ------- Tuple[np.ndarray, np.ndarray] Pair of * differences between the calculated property and expected property * weights for this dataset """ if approximate_equilibrium: _equilibrium = no_op_equilibrium_ else: _equilibrium = equilibrium_ dbf = eqpropdata.dbf species = eqpropdata.species phases = eqpropdata.phases pot_conds = eqpropdata.potential_conds models = eqpropdata.models phase_records = eqpropdata.phase_records update_phase_record_parameters(phase_records, parameters) params_dict = OrderedDict(zip(map(str, eqpropdata.params_keys), parameters)) output = eqpropdata.output weights = np.array(eqpropdata.weight, dtype=np.float) samples = np.array(eqpropdata.samples, dtype=np.float) calculated_data = [] for comp_conds in eqpropdata.composition_conds: cond_dict = OrderedDict(**pot_conds, **comp_conds) # str_statevar_dict must be sorted, assumes that pot_conds are. str_statevar_dict = OrderedDict([(str(key), vals) for key, vals in pot_conds.items()]) grid = calculate_(dbf, species, phases, str_statevar_dict, models, phase_records, pdens=500, fake_points=True) multi_eqdata = _equilibrium(species, phase_records, cond_dict, grid) # TODO: could be kind of slow. Callables (which are cachable) must be built. propdata = _eqcalculate(dbf, species, phases, cond_dict, output, data=multi_eqdata, per_phase=False, callables=None, parameters=params_dict, model=models) if 'vertex' in propdata.data_vars[output][0]: raise ValueError( f"Property {output} cannot be used to calculate equilibrium thermochemical error because each phase has a unique value for this property." ) vals = getattr(propdata, output).flatten().tolist() calculated_data.extend(vals) calculated_data = np.array(calculated_data, dtype=np.float) assert calculated_data.shape == samples.shape, f"Calculated data shape {calculated_data.shape} does not match samples shape {samples.shape}" assert calculated_data.shape == weights.shape, f"Calculated data shape {calculated_data.shape} does not match weights shape {weights.shape}" differences = calculated_data - samples logging.debug( f'Equilibrium thermochemical error - output: {output} differences: {differences}, weights: {weights}, reference: {eqpropdata.reference}' ) return differences, weights
def estimate_hyperplane(phase_region: PhaseRegion, parameters: np.ndarray, approximate_equilibrium: bool = False) -> np.ndarray: """ Calculate the chemical potentials for the target hyperplane, one vertex at a time Notes ----- This takes just *one* set of phase equilibria, a phase region, e.g. a dataset point of [['FCC_A1', ['CU'], [0.1]], ['LAVES_C15', ['CU'], [0.3]]] and calculates the chemical potentials given all the phases possible at the given compositions. Then the average chemical potentials of each end point are taken as the target hyperplane for the given equilibria. """ if approximate_equilibrium: _equilibrium = no_op_equilibrium_ else: _equilibrium = equilibrium_ target_hyperplane_chempots = [] target_hyperplane_phases = [] species = phase_region.species phases = phase_region.phases models = phase_region.models for vertex in phase_region.vertices: phase_records = vertex.phase_records update_phase_record_parameters(phase_records, parameters) cond_dict = {**vertex.comp_conds, **phase_region.potential_conds} if vertex.has_missing_comp_cond: # This composition is unknown -- it doesn't contribute to hyperplane estimation pass else: # Extract chemical potential hyperplane from multi-phase calculation # Note that we consider all phases in the system, not just ones in this tie region str_statevar_dict = OrderedDict([ (str(key), cond_dict[key]) for key in sorted(phase_region.potential_conds.keys(), key=str) ]) grid = calculate_(species, phases, str_statevar_dict, models, phase_records, pdens=50, fake_points=True) multi_eqdata = _equilibrium(phase_records, cond_dict, grid) target_hyperplane_phases.append(multi_eqdata.Phase.squeeze()) # Does there exist only a single phase in the result with zero internal degrees of freedom? # We should exclude those chemical potentials from the average because they are meaningless. num_phases = np.sum(multi_eqdata.Phase.squeeze() != '') Y_values = multi_eqdata.Y.squeeze() no_internal_dof = np.all((np.isclose(Y_values, 1.)) | np.isnan(Y_values)) MU_values = multi_eqdata.MU.squeeze() if (num_phases == 1) and no_internal_dof: target_hyperplane_chempots.append( np.full_like(MU_values, np.nan)) else: target_hyperplane_chempots.append(MU_values) target_hyperplane_mean_chempots = np.nanmean(target_hyperplane_chempots, axis=0, dtype=np.float_) return target_hyperplane_mean_chempots
def driving_force_to_hyperplane( target_hyperplane_chempots: np.ndarray, comps: Sequence[str], phase_region: PhaseRegion, vertex_idx: int, parameters: np.ndarray, approximate_equilibrium: bool = False) -> float: """Calculate the integrated driving force between the current hyperplane and target hyperplane. """ if approximate_equilibrium: _equilibrium = no_op_equilibrium_ else: _equilibrium = equilibrium_ dbf = phase_region.dbf species = phase_region.species phases = phase_region.phases models = phase_region.models current_phase = phase_region.region_phases[vertex_idx] cond_dict = { **phase_region.potential_conds, **phase_region.comp_conds[vertex_idx] } str_statevar_dict = OrderedDict([ (str(key), cond_dict[key]) for key in sorted(phase_region.potential_conds.keys(), key=str) ]) phase_flag = phase_region.phase_flags[vertex_idx] phase_records = phase_region.phase_records[vertex_idx] update_phase_record_parameters(phase_records, parameters) for key, val in cond_dict.items(): if val is None: cond_dict[key] = np.nan if np.any(np.isnan(list(cond_dict.values()))): # We don't actually know the phase composition here, so we estimate it single_eqdata = calculate_(dbf, species, [current_phase], str_statevar_dict, models, phase_records, pdens=500) df = np.multiply(target_hyperplane_chempots, single_eqdata.X).sum(axis=-1) - single_eqdata.GM driving_force = float(df.max()) elif phase_flag == 'disordered': # Construct disordered sublattice configuration from composition dict # Compute energy # Compute residual driving force # TODO: Check that it actually makes sense to declare this phase 'disordered' num_dof = sum([ len(set(c).intersection(species)) for c in dbf.phases[current_phase].constituents ]) desired_sitefracs = np.ones(num_dof, dtype=np.float) dof_idx = 0 for c in dbf.phases[current_phase].constituents: dof = sorted(set(c).intersection(comps)) if (len(dof) == 1) and (dof[0] == 'VA'): return 0 # If it's disordered config of BCC_B2 with VA, disordered config is tiny vacancy count sitefracs_to_add = np.array([cond_dict.get(v.X(d)) for d in dof], dtype=np.float) # Fix composition of dependent component sitefracs_to_add[np.isnan( sitefracs_to_add)] = 1 - np.nansum(sitefracs_to_add) desired_sitefracs[dof_idx:dof_idx + len(dof)] = sitefracs_to_add dof_idx += len(dof) single_eqdata = calculate_(dbf, species, [current_phase], str_statevar_dict, models, phase_records, pdens=500) driving_force = np.multiply( target_hyperplane_chempots, single_eqdata.X).sum(axis=-1) - single_eqdata.GM driving_force = float(np.squeeze(driving_force)) else: # Extract energies from single-phase calculations grid = calculate_(dbf, species, [current_phase], str_statevar_dict, models, phase_records, pdens=500, fake_points=True) single_eqdata = _equilibrium(species, phase_records, cond_dict, grid) if np.all(np.isnan(single_eqdata.NP)): logging.debug( 'Calculation failure: all NaN phases with phases: {}, conditions: {}, parameters {}' .format(current_phase, cond_dict, parameters)) return np.inf select_energy = float(single_eqdata.GM) region_comps = [] for comp in [c for c in sorted(comps) if c != 'VA']: region_comps.append(cond_dict.get(v.X(comp), np.nan)) region_comps[region_comps.index(np.nan)] = 1 - np.nansum(region_comps) driving_force = np.multiply(target_hyperplane_chempots, region_comps).sum() - select_energy driving_force = float(driving_force) return driving_force