def test_mole_and_mass_fraction_conversions(): """Test mole <-> mass conversions work as expected.""" # Passing database as a mass dict works dbf = Database(CUO_TDB) mole_fracs = {v.X('O'): 0.5} mass_fracs = v.get_mass_fractions(mole_fracs, v.Species('CU'), dbf) assert np.isclose(mass_fracs[v.W('O')], 0.20113144) # TC # Conversion back works round_trip_mole_fracs = v.get_mole_fractions(mass_fracs, 'CU', dbf) assert all( np.isclose(round_trip_mole_fracs[mf], mole_fracs[mf]) for mf in round_trip_mole_fracs.keys()) # Using Thermo-Calc's define components to define Al2O3 and TiO2 # Mass dict defined by hand md = {'AL': 26.982, 'TI': 47.88, 'O': 15.999} alumina = v.Species('AL2O3') mass_fracs = {v.W(alumina): 0.81, v.W("TIO2"): 0.13} mole_fracs = v.get_mole_fractions(mass_fracs, 'O', md) assert np.isclose(mole_fracs[v.X('AL2O3')], 0.59632604) # TC assert np.isclose(mole_fracs[v.X('TIO2')], 0.12216562) # TC # Conversion back works round_trip_mass_fracs = v.get_mass_fractions(mole_fracs, v.Species('O'), md) assert all( np.isclose(round_trip_mass_fracs[mf], mass_fracs[mf]) for mf in round_trip_mass_fracs.keys())
def test_database_initialization_custom_refstate(): """Test that a custom reference state with ficticious pure elements can be used to construct a Database""" refdata_stable = { "Q": Piecewise((symengine.oo, True)), "ZX": Piecewise((symengine.oo, True)), } refdata = { ("Q", "ALPHA"): Symbol("GHSERQQ"), ("Q", "BETA"): Symbol("GHSERQQ") + 10000.0, ("ZX", "BETA"): Symbol("GHSERZX"), } refdata_ser = { 'Q': { 'phase': 'ALPHA', 'mass': 8.0, 'H298': 80.0, 'S298': 0.80 }, 'ZX': { 'phase': 'BETA', 'mass': 52.0, 'H298': 520.0, 'S298': 5.20 }, } # Setup refdata CUSTOM_REFDATA_NAME = "CUSTOM" setattr(espei.refdata, CUSTOM_REFDATA_NAME + "Stable", refdata_stable) setattr(espei.refdata, CUSTOM_REFDATA_NAME, refdata) setattr(espei.refdata, CUSTOM_REFDATA_NAME + "SER", refdata_ser) # Test phase_models = { "components": ["Q", "ZX"], "phases": { "ALPHA": { "sublattice_model": [["Q"]], "sublattice_site_ratios": [1], }, "BCC": { "aliases": ["BETA"], "sublattice_model": [["Q", "ZX"]], "sublattice_site_ratios": [1.0], }, } } dbf = initialize_database(phase_models, CUSTOM_REFDATA_NAME) assert set(dbf.phases.keys()) == {"ALPHA", "BCC"} assert dbf.elements == {"Q", "ZX"} assert dbf.species == {v.Species("Q"), v.Species("ZX")} assert 'GHSERQQ' in dbf.symbols assert 'GHSERZX' in dbf.symbols assert dbf.refstates["Q"]["phase"] == "ALPHA" assert dbf.refstates["ZX"]["phase"] == "BCC" # Teardown refdata delattr(espei.refdata, CUSTOM_REFDATA_NAME + "Stable") delattr(espei.refdata, CUSTOM_REFDATA_NAME) delattr(espei.refdata, CUSTOM_REFDATA_NAME + "SER")
def unpack_components(dbf, comps): """ Parameters ---------- dbf : Database Thermodynamic database containing elements and species. comps : list Names of components to consider in the calculation. Returns ------- set Set of Species objects """ # Constrain possible components to those within phase's d.o.f # Assume for the moment that comps contains a list of pure element strings # We want to add all the species which can be created by a combination of # the user-specified pure elements species_dict = {s.name: s for s in dbf.species} possible_comps = {v.Species(species_dict.get(x, x)) for x in comps} desired_active_pure_elements = [ list(x.constituents.keys()) for x in possible_comps ] # Flatten nested list desired_active_pure_elements = [ el.upper() for constituents in desired_active_pure_elements for el in constituents ] eligible_species_from_database = { x for x in dbf.species if set(x.constituents.keys()).issubset(desired_active_pure_elements) } return eligible_species_from_database
def moles(self, species): "Number of moles of species or elements." species = v.Species(species) is_pure_element = (len(species.constituents.keys()) == 1 and list(species.constituents.keys())[0] == species.name) result = S.Zero normalization = S.Zero if is_pure_element: element = list(species.constituents.keys())[0] for idx, sublattice in enumerate(self.constituents): active = set(sublattice).intersection(self.components) result += self.site_ratios[idx] * \ sum(int(spec.number_of_atoms > 0) * spec.constituents.get(element, 0) * v.SiteFraction(self.phase_name, idx, spec) for spec in active) normalization += self.site_ratios[idx] * \ sum(spec.number_of_atoms * v.SiteFraction(self.phase_name, idx, spec) for spec in active) else: for idx, sublattice in enumerate(self.constituents): active = set(sublattice).intersection({species}) if len(active) == 0: continue result += self.site_ratios[idx] * sum(v.SiteFraction(self.phase_name, idx, spec) for spec in active) normalization += self.site_ratios[idx] * \ sum(int(spec.number_of_atoms > 0) * v.SiteFraction(self.phase_name, idx, spec) for spec in active) return result / normalization
def _param_present_in_database(dbf, phase_name, configuration, param_type): const_arr = tuple([tuple(map(lambda x: v.Species(x), subl)) for subl in map(tuplify, configuration)]) # parameter order doesn't matter here, since the generated might not exactly match. Always override. query = (where('phase_name') == phase_name) & \ (where('parameter_type') == param_type) & \ (where('constituent_array') == const_arr) search_result = dbf._parameters.search(query) if len(search_result) > 0: return True
def test_reference_energy_of_unary_twostate_einstein_magnetic_is_zero(): """The referenced energy for the pure elements in a unary Model with twostate and Einstein contributions referenced to that phase is zero.""" m = Model(FEMN_DBF, ['FE', 'VA'], 'LIQUID') statevars = { v.T: 298.15, v.SiteFraction('LIQUID', 0, 'FE'): 1, v.SiteFraction('LIQUID', 1, 'VA'): 1 } refstates = [ReferenceState(v.Species('FE'), 'LIQUID')] m.shift_reference_state(refstates, FEMN_DBF) check_output(m, statevars, 'GMR', 0.0)
def _array_validity(self, constituent_array): """ Check that the current array contains only active species. """ if len(constituent_array) != len(self.constituents): return False for sublattice in constituent_array: valid = set(sublattice).issubset(self.components) \ or sublattice[0] == v.Species('*') if not valid: return False return True
def _purity_test(self, constituent_array): """ Check if constituent array only has one species in its array This species must also be an active species """ if len(constituent_array) != len(self.constituents): return False for sublattice in constituent_array: if len(sublattice) != 1: return False if (sublattice[0] not in self.components) and \ (sublattice[0] != v.Species('*')): return False return True
def test_order_disorder_magnetic_ordering(): """Test partitioned order-disorder models with magnetic ordering contributions""" mod = Model(AL_C_FE_B2_DBF, ['AL', 'C', 'FE', 'VA'], 'B2_BCC') subs_dict = { v.Y('B2_BCC', 0, v.Species('AL')): 0.23632422, v.Y('B2_BCC', 0, v.Species('FE')): 0.09387751, v.Y('B2_BCC', 0, v.Species('VA')): 0.66979827, v.Y('B2_BCC', 1, v.Species('AL')): 0.40269437, v.Y('B2_BCC', 1, v.Species('FE')): 0.55906662, v.Y('B2_BCC', 1, v.Species('VA')): 0.03823901, v.Y('B2_BCC', 2, v.Species('C')): 0.12888967, v.Y('B2_BCC', 2, v.Species('VA')): 0.87111033, v.T: 300.0, } check_output(mod, subs_dict, 'TC', 318.65374, mode='sympy') check_output(mod, subs_dict, 'BMAG', 0.81435207, mode='sympy') check_energy(mod, subs_dict, 34659.484, mode='sympy')
def test_phase_records_are_picklable(): dof = np.array([300, 1.0]) mod = Model(ALNIPT_DBF, ['AL'], 'LIQUID') prxs = build_phase_records(ALNIPT_DBF, [v.Species('AL')], ['LIQUID'], {v.T: 300}, {'LIQUID': mod}, build_gradients=True, build_hessians=True) prx_liquid = prxs['LIQUID'] out = np.array([0.0]) prx_liquid.obj(out, dof) prx_loaded = pickle.loads(pickle.dumps(prx_liquid)) out_unpickled = np.array([0.0]) prx_loaded.obj(out_unpickled, dof) assert np.isclose(out_unpickled[0], -1037.653911) assert np.all(out == out_unpickled)
def _interaction_test(self, constituent_array): """ Check if constituent array has more than one active species in its array for at least one sublattice. """ result = False if len(constituent_array) != len(self.constituents): return False for sublattice in constituent_array: # check if all elements involved are also active valid = set(sublattice).issubset(self.components) \ or sublattice[0] == v.Species('*') if len(sublattice) > 1 and valid: result = True if not valid: result = False break return result
def redlich_kister_sum(self, phase, param_search, param_query): """ Construct parameter in Redlich-Kister polynomial basis, using the Muggianu ternary parameter extension. """ rk_terms = [] # search for desired parameters params = param_search(param_query) for param in params: # iterate over every sublattice mixing_term = S.One for subl_index, comps in enumerate(param['constituent_array']): comp_symbols = None # convert strings to symbols if comps[0] == v.Species('*'): # Handle wildcards in constituent array comp_symbols = \ [ v.SiteFraction(phase.name, subl_index, comp) for comp in sorted(set(phase.constituents[subl_index])\ .intersection(self.components)) ] mixing_term *= Add(*comp_symbols) else: comp_symbols = \ [ v.SiteFraction(phase.name, subl_index, comp) for comp in comps ] mixing_term *= Mul(*comp_symbols) # is this a higher-order interaction parameter? if len(comps) == 2 and param['parameter_order'] > 0: # interacting sublattice, add the interaction polynomial mixing_term *= Pow(comp_symbols[0] - \ comp_symbols[1], param['parameter_order']) if len(comps) == 3: # 'parameter_order' is an index to a variable when # we are in the ternary interaction parameter case # NOTE: The commercial software packages seem to have # a "feature" where, if only the zeroth # parameter_order term of a ternary parameter is specified, # the other two terms are automatically generated in order # to make the parameter symmetric. # In other words, specifying only this parameter: # PARAMETER G(FCC_A1,AL,CR,NI;0) 298.15 +30300; 6000 N ! # Actually implies: # PARAMETER G(FCC_A1,AL,CR,NI;0) 298.15 +30300; 6000 N ! # PARAMETER G(FCC_A1,AL,CR,NI;1) 298.15 +30300; 6000 N ! # PARAMETER G(FCC_A1,AL,CR,NI;2) 298.15 +30300; 6000 N ! # # If either 1 or 2 is specified, no implicit parameters are # generated. # We need to handle this case. if param['parameter_order'] == 0: # are _any_ of the other parameter_orders specified? ternary_param_query = ( (where('phase_name') == param['phase_name']) & \ (where('parameter_type') == \ param['parameter_type']) & \ (where('constituent_array') == \ param['constituent_array']) ) other_tern_params = param_search(ternary_param_query) if len(other_tern_params) == 1 and \ other_tern_params[0] == param: # only the current parameter is specified # We need to generate the other two parameters. order_one = copy.deepcopy(param) order_one['parameter_order'] = 1 order_two = copy.deepcopy(param) order_two['parameter_order'] = 2 # Add these parameters to our iteration. params.extend((order_one, order_two)) # Include variable indicated by parameter order index # Perform Muggianu adjustment to site fractions mixing_term *= comp_symbols[param['parameter_order']].subs( self._Muggianu_correction_dict(comp_symbols), simultaneous=True) if phase.model_hints.get('ionic_liquid_2SL', False): # Special normalization rules for parameters apply under this model # Reference: Bo Sundman, "Modification of the two-sublattice model for liquids", # Calphad, Volume 15, Issue 2, 1991, Pages 109-119, ISSN 0364-5916 if not any([m.species.charge < 0 for m in mixing_term.free_symbols]): pair_rule = {} # Cation site fractions must always appear with vacancy site fractions va_subls = [(v.Species('VA') in phase.constituents[idx]) for idx in range(len(phase.constituents))] va_subl_idx = (len(phase.constituents) - 1) - va_subls[::-1].index(True) va_present = any((v.Species('VA') in c) for c in param['constituent_array']) if va_present and (max(len(c) for c in param['constituent_array']) == 1): # No need to apply pair rule for VA-containing endmember pass elif va_subl_idx > -1: for sym in mixing_term.free_symbols: if sym.species.charge > 0: pair_rule[sym] = sym * v.SiteFraction(sym.phase_name, va_subl_idx, v.Species('VA')) mixing_term = mixing_term.xreplace(pair_rule) # This parameter is normalized differently due to the variable charge valence of vacancies mixing_term *= self.site_ratios[va_subl_idx] rk_terms.append(mixing_term * param['parameter']) return Add(*rk_terms)
def test_order_disorder_interstitial_sublattice(): """Test that non-vacancy elements are supported on interstitial sublattices""" TDB_OrderDisorder_VA_VA = """ ELEMENT VA VACUUM 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT A DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT B DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT C DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! DEFINE_SYSTEM_DEFAULT ELEMENT 2 ! DEFAULT_COMMAND DEF_SYS_ELEMENT VA ! TYPE_DEFINITION % SEQ *! TYPE_DEFINITION & GES A_P_D DISORD MAGNETIC -1.0 4.00000E-01 ! TYPE_DEFINITION ( GES A_P_D ORDERED MAGNETIC -1.0 4.00000E-01 ! TYPE_DEFINITION ' GES A_P_D ORDERED DIS_PART DISORD ,,,! PHASE DISORD %& 2 1 3 ! PHASE ORDERED %(' 3 0.5 0.5 3 ! CONSTITUENT DISORD : A,B,VA : VA : ! CONSTITUENT ORDERED : A,B,VA : A,B,VA : VA : ! PARAMETER G(DISORD,A:VA;0) 298.15 -10000; 6000 N ! PARAMETER G(DISORD,B:VA;0) 298.15 -10000; 6000 N ! """ TDB_OrderDisorder_VA_VA_C = """ $ Compared to TDB_OrderDisorder_VA_VA, this database only $ differs in the fact that the VA sublattice contains C ELEMENT VA VACUUM 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT A DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT B DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT C DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! DEFINE_SYSTEM_DEFAULT ELEMENT 2 ! DEFAULT_COMMAND DEF_SYS_ELEMENT VA ! TYPE_DEFINITION % SEQ *! TYPE_DEFINITION & GES A_P_D DISORD MAGNETIC -1.0 4.00000E-01 ! TYPE_DEFINITION ( GES A_P_D ORDERED MAGNETIC -1.0 4.00000E-01 ! TYPE_DEFINITION ' GES A_P_D ORDERED DIS_PART DISORD ,,,! PHASE DISORD %& 2 1 3 ! PHASE ORDERED %(' 3 0.5 0.5 3 ! CONSTITUENT DISORD : A,B,VA : C,VA : ! CONSTITUENT ORDERED : A,B,VA : A,B,VA : C,VA : ! PARAMETER G(DISORD,A:VA;0) 298.15 -10000; 6000 N ! PARAMETER G(DISORD,B:VA;0) 298.15 -10000; 6000 N ! """ TDB_OrderDisorder_VA_C = """ $ Compared to TDB_OrderDisorder_VA_VA_C, this database only $ differs in the fact that the disorderd sublattices do not contain VA ELEMENT VA VACUUM 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT A DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT B DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT C DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! DEFINE_SYSTEM_DEFAULT ELEMENT 2 ! DEFAULT_COMMAND DEF_SYS_ELEMENT VA ! TYPE_DEFINITION % SEQ *! TYPE_DEFINITION & GES A_P_D DISORD MAGNETIC -1.0 4.00000E-01 ! TYPE_DEFINITION ( GES A_P_D ORDERED MAGNETIC -1.0 4.00000E-01 ! TYPE_DEFINITION ' GES A_P_D ORDERED DIS_PART DISORD ,,,! PHASE DISORD %& 2 1 3 ! PHASE ORDERED %(' 3 0.5 0.5 3 ! CONSTITUENT DISORD : A,B : C,VA : ! CONSTITUENT ORDERED : A,B : A,B : C,VA : ! PARAMETER G(DISORD,A:VA;0) 298.15 -10000; 6000 N ! PARAMETER G(DISORD,B:VA;0) 298.15 -10000; 6000 N ! """ TDB_OrderDisorder_VA_B = """ $ This database contains B in both substitutional and interstitial sublattices ELEMENT VA VACUUM 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT A DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT B DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! ELEMENT C DISORD 0.0000E+00 0.0000E+00 0.0000E+00 ! DEFINE_SYSTEM_DEFAULT ELEMENT 2 ! DEFAULT_COMMAND DEF_SYS_ELEMENT VA ! TYPE_DEFINITION % SEQ *! TYPE_DEFINITION & GES A_P_D DISORD MAGNETIC -1.0 4.00000E-01 ! TYPE_DEFINITION ( GES A_P_D ORDERED MAGNETIC -1.0 4.00000E-01 ! TYPE_DEFINITION ' GES A_P_D ORDERED DIS_PART DISORD ,,,! PHASE DISORD %& 2 1 3 ! PHASE ORDERED %(' 3 0.5 0.5 3 ! CONSTITUENT DISORD : A,B : B,VA : ! CONSTITUENT ORDERED : A,B : A,B : B,VA : ! PARAMETER G(DISORD,A:VA;0) 298.15 -10000; 6000 N ! PARAMETER G(DISORD,B:VA;0) 298.15 -10000; 6000 N ! PARAMETER G(DISORD,A:B;0) 298.15 -20000; 6000 N ! PARAMETER G(DISORD,B:B;0) 298.15 -20000; 6000 N ! PARAMETER G(ORDERED,A:B:B;0) 298.15 -1000; 6000 N ! PARAMETER G(ORDERED,A:B:VA;0) 298.15 -2000; 6000 N ! """ db_VA_VA = Database(TDB_OrderDisorder_VA_VA) db_VA_C = Database(TDB_OrderDisorder_VA_C) db_VA_VA_C = Database(TDB_OrderDisorder_VA_VA_C) mod_VA_VA = Model(db_VA_VA, ["A", "B", "VA"], "ORDERED") mod_VA_C = Model(db_VA_C, ["A", "B", "VA"], "ORDERED") mod_VA_VA_C = Model(db_VA_VA_C, ["A", "B", "VA"], "ORDERED") # Site fractions for pure A subs_dict = { v.Y('ORDERED', 0, v.Species('A')): 1.0, v.Y('ORDERED', 0, v.Species('B')): 0.0, v.Y('ORDERED', 0, v.Species('VA')): 0.0, v.Y('ORDERED', 1, v.Species('A')): 1.0, v.Y('ORDERED', 1, v.Species('B')): 0.0, v.Y('ORDERED', 1, v.Species('VA')): 0.0, v.Y('ORDERED', 2, v.Species('VA')): 1.0, v.T: 300.0, } check_energy(mod_VA_VA, subs_dict, -10000, mode='sympy') check_energy(mod_VA_C, subs_dict, -10000, mode='sympy') check_energy(mod_VA_VA_C, subs_dict, -10000, mode='sympy') db_VA_B = Database(TDB_OrderDisorder_VA_B) mod_VA_B = Model(db_VA_B, ["A", "B", "VA"], "ORDERED") # A-B disordered substitutional disord_subs_dict = { v.Y('ORDERED', 0, v.Species('A')): 0.5, v.Y('ORDERED', 0, v.Species('B')): 0.5, v.Y('ORDERED', 1, v.Species('A')): 0.5, v.Y('ORDERED', 1, v.Species('B')): 0.5, v.Y('ORDERED', 2, v.Species('B')): 0.25, v.Y('ORDERED', 2, v.Species('VA')): 0.75, v.T: 300.0, } # Thermo-Calc energy via set-start-constitution check_energy(mod_VA_B, disord_subs_dict, -10535.395, mode='sympy') # A-B ordered substitutional ord_subs_dict = { v.Y('ORDERED', 0, v.Species('A')): 1.0, v.Y('ORDERED', 0, v.Species('B')): 0.0, v.Y('ORDERED', 1, v.Species('A')): 0.0, v.Y('ORDERED', 1, v.Species('B')): 1.0, v.Y('ORDERED', 2, v.Species('B')): 0.25, v.Y('ORDERED', 2, v.Species('VA')): 0.75, v.T: 300.0, } # Thermo-Calc energy via set-start-constitution check_energy(mod_VA_B, ord_subs_dict, -10297.421, mode='sympy')
def test_ionic_liquid_energy_anion_sublattice(): """Test that having anions, vacancies, and neutral species in the anion sublattice of a two sublattice ionic liquid produces the correct Gibbs energy""" # Uses the sublattice model (FE+2)(S-2, VA, S) mod = Model(FE_MN_S_DBF, ['FE', 'S', 'VA'], 'IONIC_LIQ') # Same potentials for all test cases here potentials = {v.P: 101325, v.T: 1600} # All values checked by Thermo-Calc using set-start-constitution and show gm(ionic_liq) # Test the three endmembers produce the corect energy em_FE_Sneg2 = { v.Y('IONIC_LIQ', 0, v.Species('FE+2', {'FE': 1.0}, charge=2)): 1.0, v.Y('IONIC_LIQ', 1, v.Species('S-2', {'S': 1.0}, charge=-2)): 1.0, v.Y('IONIC_LIQ', 1, v.Species('VA')): 1e-12, v.Y('IONIC_LIQ', 1, v.Species('S', {'S': 1.0})): 1e-12, } out = np.array(mod.ast.subs({ **potentials, **em_FE_Sneg2 }).n(real=True), dtype=np.complex_) assert np.isclose(out, -148395.0, atol=0.1) em_FE_VA = { v.Y('IONIC_LIQ', 0, v.Species('FE+2', {'FE': 1.0}, charge=2)): 1.0, v.Y('IONIC_LIQ', 1, v.Species('S-2', {'S': 1.0}, charge=-2)): 1e-12, v.Y('IONIC_LIQ', 1, v.Species('VA')): 1.0, v.Y('IONIC_LIQ', 1, v.Species('S', {'S': 1.0})): 1e-12, } out = np.array(mod.ast.subs({ **potentials, **em_FE_VA }).n(real=True), dtype=np.complex_) assert np.isclose(out, -87735.077, atol=0.1) em_FE_S = { v.Y('IONIC_LIQ', 0, v.Species('FE+2', {'FE': 1.0}, charge=2)): 1.0, v.Y('IONIC_LIQ', 1, v.Species('S-2', {'S': 1.0}, charge=-2)): 1e-12, v.Y('IONIC_LIQ', 1, v.Species('VA')): 1e-12, v.Y('IONIC_LIQ', 1, v.Species('S', {'S': 1.0})): 1.0, } out = np.array(mod.ast.subs({ **potentials, **em_FE_S }).n(real=True), dtype=np.complex_) assert np.isclose(out, -102463.52, atol=0.1) # Test some ficticious "nice" mixing cases mix_equal = { v.Y('IONIC_LIQ', 0, v.Species('FE+2', {'FE': 1.0}, charge=2)): 1.0, v.Y('IONIC_LIQ', 1, v.Species('S-2', {'S': 1.0}, charge=-2)): 0.33333333, v.Y('IONIC_LIQ', 1, v.Species('VA')): 0.33333333, v.Y('IONIC_LIQ', 1, v.Species('S', {'S': 1.0})): 0.33333333, } out = np.array(mod.ast.subs({ **potentials, **mix_equal }).n(real=True), dtype=np.complex_) assert np.isclose(out, -130358.2, atol=0.1) mix_unequal = { v.Y('IONIC_LIQ', 0, v.Species('FE+2', {'FE': 1.0}, charge=2)): 1.0, v.Y('IONIC_LIQ', 1, v.Species('S-2', {'S': 1.0}, charge=-2)): 0.5, v.Y('IONIC_LIQ', 1, v.Species('VA')): 0.25, v.Y('IONIC_LIQ', 1, v.Species('S', {'S': 1.0})): 0.25, } out = np.array(mod.ast.subs({ **potentials, **mix_unequal }).n(real=True), dtype=np.complex_) assert np.isclose(out, -138484.11, atol=0.1) # Test the energies for the two equilibrium internal DOF for the conditions eq_sf_1 = { v.Y('IONIC_LIQ', 0, v.Species('FE+2', {'FE': 1.0}, charge=2)): 1.0, v.Y('IONIC_LIQ', 1, v.Species('S', {'S': 1.0})): 3.98906E-01, v.Y('IONIC_LIQ', 1, v.Species('VA')): 1.00545E-04, v.Y('IONIC_LIQ', 1, v.Species('S-2', {'S': 1.0}, charge=-2)): 6.00994E-01, } out = np.array(mod.ast.subs({ **potentials, **eq_sf_1 }).n(real=True), dtype=np.complex_) assert np.isclose(out, -141545.37, atol=0.1) eq_sf_2 = { v.Y('IONIC_LIQ', 0, v.Species('FE+2', charge=2)): 1.0, v.Y('IONIC_LIQ', 1, v.Species('S-2', charge=-2)): 1.53788E-02, v.Y('IONIC_LIQ', 1, v.Species('VA')): 1.45273E-04, v.Y('IONIC_LIQ', 1, v.Species('S')): 9.84476E-01, } out = np.array(mod.ast.subs({ **potentials, **eq_sf_2 }).n(real=True), dtype=np.complex_) assert np.isclose(out, -104229.18, atol=0.1)
def test_underspecified_refstate_raises(): """A Model cannot be shifted to a new reference state unless references for all pure elements are specified.""" m = Model(FEMN_DBF, ['FE', 'MN', 'VA'], 'LIQUID') refstates = [ReferenceState(v.Species('FE'), 'LIQUID')] with pytest.raises(DofError): m.shift_reference_state(refstates, FEMN_DBF)
def test_species_parse_unicode_strings(): """Species should properly parse unicode strings.""" s = v.Species(u"MG")
def initialize_database(phase_models, ref_state, dbf=None, fallback_ref_state="SGTE91"): """Return a Database boostraped with elements, species, phases and unary lattice stabilities. Parameters ---------- phase_models : Dict[str, Any] Dictionary of components and phases to fit. ref_state : str String of the reference data to use, e.g. 'SGTE91' or 'SR2016' dbf : Optional[Database] Initial pycalphad Database that can have parameters that would not be fit by ESPEI fallback_ref_state : str String of the reference data to use for SER data, defaults to 'SGTE91' Returns ------- Database A new pycalphad Database object, or a modified one if it was given. """ if dbf is None: dbf = Database() lattice_stabilities = getattr(espei.refdata, ref_state) ser_stability = getattr(espei.refdata, ref_state + "Stable") aliases = extract_aliases(phase_models) phases = sorted({ph.upper() for ph in phase_models["phases"].keys()}) elements = {el.upper() for el in phase_models["components"]} dbf.elements.update(elements) dbf.species.update({v.Species(el, {el: 1}, 0) for el in elements}) # Add SER reference data for this element for element in dbf.elements: if element in dbf.refstates: continue # Do not clobber user reference states el_ser_data = _get_ser_data(element, ref_state, fallback_ref_state=fallback_ref_state) # Try to look up the alias that we are using in this fitting el_ser_data["phase"] = aliases.get(el_ser_data["phase"], el_ser_data["phase"]) # Don't warn if the element is a species with no atoms because per-atom # formation energies are not possible (e.g. VA (VACUUM) or /- (ELECTRON_GAS)) if el_ser_data["phase"] not in phases and v.Species( element).number_of_atoms != 0: # We have the Gibbs energy expression that we need in the reference # data, but this phase is not a candidate in the phase models. The # phase won't be added to the database, so looking up the phases's # energy won't work. _log.warning( "The reference phase for %s, %s, is not in the supplied phase models " "and won't be added to the Database phases. Fitting formation " "energies will not be possible.", element, el_ser_data["phase"]) dbf.refstates[element] = el_ser_data # Add the phases for phase_name, phase_data in phase_models['phases'].items(): if phase_name not in dbf.phases.keys(): # Do not clobber user phases # TODO: Need to support model hints for: magnetic, order-disorder, etc. site_ratios = phase_data['sublattice_site_ratios'] subl_model = phase_data['sublattice_model'] # Only generate the sublattice model for active components subl_model = [ sorted(set(subl).intersection(dbf.elements)) for subl in subl_model ] if all(len(subl) > 0 for subl in subl_model): dbf.add_phase(phase_name, dict(), site_ratios) dbf.add_phase_constituents(phase_name, subl_model) # Add the GHSER functions to the Database for element in dbf.elements: # Use setdefault here to not clobber user-provided functions if element == "VA": dbf.symbols.setdefault("GHSERVA", 0) else: # note that `c.upper()*2)[:2]` returns "AL" for "Al" and "BB" for "B" # Using this ensures that GHSER functions will be unique, e.g. # GHSERC would be an abbreviation for GHSERCA. sym_name = "GHSER" + (element.upper() * 2)[:2] dbf.symbols.setdefault(sym_name, ser_stability[element]) return dbf
def build_eqpropdata( data: tinydb.database.Document, dbf: Database, parameters: Optional[Dict[str, float]] = None, data_weight_dict: Optional[Dict[str, float]] = None) -> EqPropData: """ Build EqPropData for the calculations corresponding to a single dataset. Parameters ---------- data : tinydb.database.Document Document corresponding to a single ESPEI dataset. dbf : Database Database that should be used to construct the `Model` and `PhaseRecord` objects. parameters : Optional[Dict[str, float]] Mapping of parameter symbols to values. data_weight_dict : Optional[Dict[str, float]] Mapping of a data type (e.g. `HM` or `SM`) to a weight. Returns ------- EqPropData """ parameters = parameters if parameters is not None else {} data_weight_dict = data_weight_dict if data_weight_dict is not None else {} property_std_deviation = { 'HM': 500.0, # J/mol 'SM': 0.2, # J/K-mol 'CPM': 0.2, # J/K-mol } params_keys, _ = extract_parameters(parameters) data_comps = list(set(data['components']).union({'VA'})) species = sorted(unpack_components(dbf, data_comps), key=str) data_phases = filter_phases(dbf, species, candidate_phases=data['phases']) models = instantiate_models(dbf, species, data_phases, parameters=parameters) output = data['output'] property_output = output.split('_')[ 0] # property without _FORM, _MIX, etc. samples = np.array(data['values']).flatten() reference = data.get('reference', '') # Models are now modified in response to the data from this data if 'reference_states' in data: property_output = output[:-1] if output.endswith( 'R' ) else output # unreferenced model property so we can tell shift_reference_state what to build. reference_states = [] for el, vals in data['reference_states'].items(): reference_states.append( ReferenceState( v.Species(el), vals['phase'], fixed_statevars=vals.get('fixed_state_variables'))) for mod in models.values(): mod.shift_reference_state(reference_states, dbf, output=(property_output, )) data['conditions'].setdefault( 'N', 1.0 ) # Add default for N. Nothing else is supported in pycalphad anyway. pot_conds = OrderedDict([(getattr(v, key), unpack_condition(data['conditions'][key])) for key in sorted(data['conditions'].keys()) if not key.startswith('X_')]) comp_conds = OrderedDict([(v.X(key[2:]), unpack_condition(data['conditions'][key])) for key in sorted(data['conditions'].keys()) if key.startswith('X_')]) phase_records = build_phase_records(dbf, species, data_phases, { **pot_conds, **comp_conds }, models, parameters=parameters, build_gradients=True, build_hessians=True) # Now we need to unravel the composition conditions # (from Dict[v.X, Sequence[float]] to Sequence[Dict[v.X, float]]), since the # composition conditions are only broadcast against the potentials, not # each other. Each individual composition needs to be computed # independently, since broadcasting over composition cannot be turned off # in pycalphad. rav_comp_conds = [ OrderedDict(zip(comp_conds.keys(), pt_comps)) for pt_comps in zip(*comp_conds.values()) ] # Build weights, should be the same size as the values total_num_calculations = len(rav_comp_conds) * np.prod( [len(vals) for vals in pot_conds.values()]) dataset_weights = np.array(data.get('weight', 1.0)) * np.ones(total_num_calculations) weights = (property_std_deviation.get(property_output, 1.0) / data_weight_dict.get(property_output, 1.0) / dataset_weights).flatten() return EqPropData(dbf, species, data_phases, pot_conds, rav_comp_conds, models, params_keys, phase_records, output, samples, weights, reference)
def __init__(self, dbe, comps, phase_name, parameters=None): self.components = set() self.constituents = [] self.phase_name = phase_name.upper() phase = dbe.phases[self.phase_name] self.site_ratios = list(phase.sublattices) for idx, sublattice in enumerate(phase.constituents): subl_comps = set(sublattice).intersection(unpack_components(dbe, comps)) self.components |= subl_comps # Support for variable site ratios in ionic liquid model if phase.model_hints.get('ionic_liquid_2SL', False): if idx == 0: subl_idx = 1 elif idx == 1: subl_idx = 0 else: raise ValueError('Two-sublattice ionic liquid specified with more than two sublattices') self.site_ratios[subl_idx] = Add(*[v.SiteFraction(self.phase_name, idx, spec) * abs(spec.charge) for spec in subl_comps]) if phase.model_hints.get('ionic_liquid_2SL', False): # Special treatment of "neutral" vacancies in 2SL ionic liquid # These are treated as having variable valence for idx, sublattice in enumerate(phase.constituents): subl_comps = set(sublattice).intersection(unpack_components(dbe, comps)) if v.Species('VA') in subl_comps: if idx == 0: subl_idx = 1 elif idx == 1: subl_idx = 0 else: raise ValueError('Two-sublattice ionic liquid specified with more than two sublattices') self.site_ratios[subl_idx] += self.site_ratios[idx] * v.SiteFraction(self.phase_name, idx, v.Species('VA')) self.site_ratios = tuple(self.site_ratios) # Verify that this phase is still possible to build for sublattice in phase.constituents: if len(set(sublattice).intersection(self.components)) == 0: # None of the components in a sublattice are active # We cannot build a model of this phase raise DofError( '{0}: Sublattice {1} of {2} has no components in {3}' \ .format(self.phase_name, sublattice, phase.constituents, self.components)) self.constituents.append(set(sublattice).intersection(self.components)) self.components = sorted(self.components) # Convert string symbol names to sympy Symbol objects # This makes xreplace work with the symbols dict symbols = {Symbol(s): val for s, val in dbe.symbols.items()} def wrap_symbol(obj): if isinstance(obj, Symbol): return obj else: return Symbol(obj) if parameters is not None: symbols.update([(wrap_symbol(s), val) for s, val in parameters.items()]) self._symbols = {wrap_symbol(key): value for key, value in symbols.items()} self.models = OrderedDict() self.build_phase(dbe) self.site_fractions = sorted([x for x in self.ast.free_symbols if isinstance(x, v.SiteFraction)], key=str) for name, value in self.models.items(): self.models[name] = self.symbol_replace(value, symbols)
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 fit_formation_energy(dbf, comps, phase_name, configuration, symmetry, datasets, ridge_alpha=1.0e-100, features=None): """ Find suitable linear model parameters for the given phase. We do this by successively fitting heat capacities, entropies and enthalpies of formation, and selecting against criteria to prevent overfitting. The "best" set of parameters minimizes the error without overfitting. Parameters ---------- dbf : Database pycalphad Database. Partially complete, so we know what degrees of freedom to fix. comps : [str] Names of the relevant components. phase_name : str Name of the desired phase for which the parameters will be found. configuration : ndarray Configuration of the sublattices for the fitting procedure. symmetry : [[int]] Symmetry of the sublattice configuration. datasets : PickleableTinyDB All the datasets desired to fit to. ridge_alpha : float Value of the $alpha$ hyperparameter used in ridge regression. Defaults to 1.0e-100, which should be degenerate with ordinary least squares regression. For now, the parameter is applied to all features. features : dict Maps "property" to a list of features for the linear model. These will be transformed from "GM" coefficients e.g., {"CPM_FORM": (v.T*sympy.log(v.T), v.T**2, v.T**-1, v.T**3)} (Default value = None) Returns ------- dict {feature: estimated_value} """ if interaction_test(configuration): logging.debug('ENDMEMBERS FROM INTERACTION: {}'.format( endmembers_from_interaction(configuration))) fitting_steps = (["CPM_FORM", "CPM_MIX"], ["SM_FORM", "SM_MIX"], ["HM_FORM", "HM_MIX"]) else: # We are only fitting an endmember; no mixing data needed fitting_steps = (["CPM_FORM"], ["SM_FORM"], ["HM_FORM"]) # create the candidate models and fitting steps if features is None: features = OrderedDict([("CPM_FORM", (v.T * sympy.log(v.T), v.T**2, v.T**-1, v.T**3)), ("SM_FORM", (v.T, )), ("HM_FORM", (sympy.S.One, ))]) # dict of {feature, [candidate_models]} candidate_models_features = build_candidate_models(configuration, features) # All possible parameter values that could be taken on. This is some legacy # code from before there were many candidate models built. For very large # sets of candidate models, this could be quite slow. # TODO: we might be able to remove this initialization for clarity, depends on fixed poritions parameters = {} for candidate_models in candidate_models_features.values(): for model in candidate_models: for coef in model: parameters[coef] = 0 # These is our previously fit partial model from previous steps # Subtract out all of these contributions (zero out reference state because these are formation properties) fixed_model = Model( dbf, comps, phase_name, parameters={'GHSER' + (c.upper() * 2)[:2]: 0 for c in comps}) fixed_model.models['idmix'] = 0 fixed_portions = [0] moles_per_formula_unit = sympy.S(0) YS = sympy.Symbol('YS') # site fraction symbol that we will reuse Z = sympy.Symbol('Z') # site fraction symbol that we will reuse subl_idx = 0 for num_sites, const in zip(dbf.phases[phase_name].sublattices, dbf.phases[phase_name].constituents): if v.Species('VA') in const: moles_per_formula_unit += num_sites * ( 1 - v.SiteFraction(phase_name, subl_idx, v.Species('VA'))) else: moles_per_formula_unit += num_sites subl_idx += 1 for desired_props in fitting_steps: desired_data = get_data(comps, phase_name, configuration, symmetry, datasets, desired_props) logging.debug('{}: datasets found: {}'.format(desired_props, len(desired_data))) if len(desired_data) > 0: # We assume all properties in the same fitting step have the same features (all CPM, all HM, etc.) (but different ref states) all_samples = get_samples(desired_data) site_fractions = [ build_sitefractions( phase_name, ds['solver']['sublattice_configurations'], ds['solver'].get( 'sublattice_occupancies', np.ones(( len(ds['solver']['sublattice_configurations']), len(ds['solver']['sublattice_configurations'][0])), dtype=np.float))) for ds in desired_data for _ in ds['conditions']['T'] ] # Flatten list site_fractions = list(itertools.chain(*site_fractions)) # build the candidate model transformation matrix and response vector (A, b in Ax=b) feature_matricies = [] data_quantities = [] for candidate_model in candidate_models_features[desired_props[0]]: if interaction_test(configuration, 3): feature_matricies.append( build_ternary_feature_matrix(desired_props[0], candidate_model, desired_data)) else: feature_matricies.append( _build_feature_matrix(desired_props[0], candidate_model, desired_data)) data_qtys = np.concatenate(shift_reference_state( desired_data, feature_transforms[desired_props[0]], fixed_model), axis=-1) # Remove existing partial model contributions from the data data_qtys = data_qtys - feature_transforms[desired_props[0]]( fixed_model.ast) # Subtract out high-order (in T) parameters we've already fit data_qtys = data_qtys - feature_transforms[desired_props[0]]( sum(fixed_portions)) / moles_per_formula_unit # if any site fractions show up in our data_qtys that aren't in this datasets site fractions, set them to zero. for sf, i, (_, (sf_product, inter_product)) in zip(site_fractions, data_qtys, all_samples): missing_variables = sympy.S( i * moles_per_formula_unit).atoms( v.SiteFraction) - set(sf.keys()) sf.update({x: 0. for x in missing_variables}) # The equations we have just have the site fractions as YS # and interaction products as Z, so take the product of all # the site fractions that we see in our data qtys sf.update({YS: sf_product, Z: inter_product}) # moles_per_formula_unit factor is here because our data is stored per-atom # but all of our fits are per-formula-unit data_qtys = [ sympy.S(i * moles_per_formula_unit).xreplace(sf).xreplace({ v.T: ixx[0] }).evalf() for i, sf, ixx in zip(data_qtys, site_fractions, all_samples) ] data_qtys = np.asarray(data_qtys, dtype=np.float) data_quantities.append(data_qtys) # provide candidate models and get back a selected model. selected_model = select_model( zip(candidate_models_features[desired_props[0]], feature_matricies, data_quantities), ridge_alpha) selected_features, selected_values = selected_model parameters.update(zip(*(selected_features, selected_values))) # Add these parameters to be fixed for the next fitting step fixed_portion = np.array(selected_features, dtype=np.object) fixed_portion = np.dot(fixed_portion, selected_values) fixed_portions.append(fixed_portion) return parameters