Exemplo n.º 1
0
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())
Exemplo n.º 2
0
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")
Exemplo n.º 3
0
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
Exemplo n.º 4
0
 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
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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)
Exemplo n.º 7
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
Exemplo n.º 8
0
 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
Exemplo n.º 9
0
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')
Exemplo n.º 10
0
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)
Exemplo n.º 11
0
 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
Exemplo n.º 12
0
    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)
Exemplo n.º 13
0
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')
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
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)
Exemplo n.º 16
0
def test_species_parse_unicode_strings():
    """Species should properly parse unicode strings."""
    s = v.Species(u"MG")
Exemplo n.º 17
0
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)
Exemplo n.º 19
0
    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)
Exemplo n.º 20
0
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
Exemplo n.º 21
0
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