def test_incompatible_model_instance_raises(): "Calculate raises when an incompatible Model instance built with a different phase is passed." comps = ['AL', 'CR', 'NI'] phase_name = 'L12_FCC' mod = Model(DBF, comps, 'LIQUID') # Model instance does not match the phase with pytest.raises(ValueError): calculate(DBF, comps, phase_name, T=1400.0, output='_fail_', model=mod)
def test_unknown_model_attribute(): "Sampling an unknown model attribute raises exception." with pytest.raises(AttributeError): calculate(DBF, ['AL', 'CR', 'NI'], 'L12_FCC', T=1400.0, output='_fail_')
def test_single_model_instance_raises(): "Calculate raises when a single Model instance is passed with multiple phases." comps = ['AL', 'CR', 'NI'] phase_name = 'L12_FCC' mod = Model(DBF, comps, 'L12_FCC') # Model instance does not match the phase with pytest.raises(ValueError): calculate(DBF, comps, ['LIQUID', 'L12_FCC'], T=1400.0, output='_fail_', model=mod)
def test_calculate_with_parameters_vectorized(): # Second set of parameter values are directly copied from the TDB parameters = { 'VV0000': [-33134.699474175846, -32539.5], 'VV0001': [7734.114029426941, 8236.3], 'VV0002': [-13498.542175596054, -14675.0], 'VV0003': [-26555.048975092268, -24441.2], 'VV0004': [20777.637577083482, 20149.6], 'VV0005': [41915.70425630003, 46500.0], 'VV0006': [-34525.21964215504, -39591.3], 'VV0007': [95457.14639216446, 104160.0], 'VV0008': [21139.578967453144, 21000.0], 'VV0009': [19047.833726419598, 17772.0], 'VV0010': [20468.91829601273, 21240.0], 'VV0011': [19601.617855958328, 14321.1], 'VV0012': [-4546.9325861738, -4923.18], 'VV0013': [-1640.6354331231278, -1962.8], 'VV0014': [-35682.950005357634, -31626.6] } res = calculate(CUMG_PARAMETERS_DBF, ['CU', 'MG'], ['HCP_A3'], parameters=parameters, T=743.15, P=1e5) res_noparams = calculate(CUMG_PARAMETERS_DBF, ['CU', 'MG'], ['HCP_A3'], parameters=None, T=743.15, P=1e5) param_values = [] for symbol in sorted(parameters.keys()): param_values.append(parameters[symbol]) param_values = np.array(param_values).T assert all( res['param_symbols'] == sorted([str(x) for x in parameters.keys()])) assert_allclose(np.squeeze(res['param_values'].values), param_values) assert_allclose(res.GM.isel(samples=1).values, res_noparams.GM.values)
def PT_phase_diagram(dbf, comps, phases, conds, x=v.T): # Compute based on crossover temperatures in calculate # Only works for systems without any internal degrees of freedom from pycalphad.core.utils import unpack_condition from collections import defaultdict T_phase_pair_lines = defaultdict(list) P_phase_pair_lines = defaultdict(list) all_phase_pairs = set() pressures = unpack_condition(conds[v.P]) for P in pressures: cr = calculate(dbf, comps, phases, P=P, T=conds[v.T], N=conds[v.N]) phase_values = cr["Phase"].values.squeeze() phase_idx = cr.GM.values.squeeze().argmin(axis=1) phase_change_temp_index = np.nonzero(phase_idx[:-1] != phase_idx[1:]) for temp_idx in phase_change_temp_index[0]: phase_pair = phase_values[0, phase_idx[temp_idx]] + ' + ' + cr["Phase"].values.squeeze()[0, phase_idx[temp_idx+1]] T_phase_pair_lines[phase_pair].append(cr["T"][temp_idx]) P_phase_pair_lines[phase_pair].append(P) all_phase_pairs |= {phase_pair} for phase_pair in all_phase_pairs: if x == v.T: plt.plot(T_phase_pair_lines[phase_pair], P_phase_pair_lines[phase_pair], label=phase_pair) elif x == v.P: plt.plot(P_phase_pair_lines[phase_pair], T_phase_pair_lines[phase_pair], label=phase_pair) if x == v.T: plt.xlabel('Temperature [K]') plt.ylabel('Pressure [Pa]') plt.xlim(conds[v.T][0], conds[v.T][1]) plt.ylim(conds[v.P][0], conds[v.P][1]) else: plt.xlabel('Pressure [Pa]') plt.ylabel('Temperature [K]') plt.xlim(conds[v.P][0], conds[v.P][1]) plt.ylim(conds[v.T][0], conds[v.T][1]) plt.legend(loc=(1.1, 0.5))
def compute_values(*args): prefill_callables = {key: functools.partial(*itertools.chain([func], args[:len(params)])) for key, func in callables.items()} result = calculate(dbf, data['components'], data['phases'], output=data['output'], points=np.atleast_2d(data['solver']['sublattice_configuration']).astype(np.float), callables=prefill_callables, model=fit_models, **extra_conds) return result
def compute_tdb_energy_nc(temps, c, phase): """ Computes Gibbs Free Energy and its derivative*S* w.r.t. composition, for a given temperature field and list of composition fields Derivatives are computed by holding all other explicit composition variables constant c_i is increased, c_N is decreased (the implicitly-defined last composition variable which equals 1-sum(c_i) ) Returns GM (Molar Gibbs Free Energy) and dGdci (list of derivatives of GM, w.r.t. c_i) """ #alphabetical order of components! fec = [] #flattened expanded c for i in range(len(c)): fec.append(np.expand_dims(c[i].flatten(), axis=1)) fec_n_comp = np.ones(fec[0].shape) for i in range(len(c)): fec_n_comp -= fec[i] for i in range(len(c)): fec_n_comp = np.concatenate((fec_n_comp, fec[i]), axis=1) #move final component to end, maybe ill find a way to write this better in the future... fec_n_comp = np.roll(fec_n_comp, -1, axis=1) #offset composition, for computing slope of GM w.r.t. comp fec_nc_offset = [] for i in range(len(c)): fec_offset = np.zeros([len(c) + 1]) fec_offset[i] = 0.0000001 fec_offset[len(c)] = -0.0000001 fec_nc_offset.append(fec_n_comp + fec_offset) flattened_t = temps.flatten() GM = pyc.calculate(tdb, components, phase, P=101325, T=flattened_t, points=fec_n_comp, broadcast=False).GM.values.reshape(c[0].shape) GM_derivs = [] for i in range(len(c)): GM_derivs.append( (pyc.calculate(tdb, components, phase, P=101325, T=flattened_t, points=fec_nc_offset[i], broadcast=False).GM.values.reshape(c[0].shape) - GM) * (10000000.)) return GM, GM_derivs
def test_issue116(): "Calculate gives correct result when a state variable is left as default (gh-116)." result_one = calculate(DBF, ['AL', 'CR', 'NI'], 'LIQUID', T=400) result_one_values = result_one.GM.values result_two = calculate(DBF, ['AL', 'CR', 'NI'], 'LIQUID', T=400, P=101325) result_two_values = result_two.GM.values result_three = calculate(DBF, ['AL', 'CR', 'NI'], 'LIQUID', T=400, P=101325, N=1) result_three_values = result_three.GM.values np.testing.assert_array_equal(np.squeeze(result_one_values), np.squeeze(result_two_values)) np.testing.assert_array_equal(np.squeeze(result_one_values), np.squeeze(result_three_values)) # N is added automatically assert len(result_one_values.shape) == 3 # N, T, points assert result_one_values.shape[0] == 1 assert len(result_two_values.shape) == 4 # N, P, T, points assert result_two_values.shape[:3] == (1, 1, 1) assert len(result_three_values.shape) == 4 # N, P, T, points assert result_three_values.shape[:3] == (1, 1, 1)
def test_eq_single_phase(): "Equilibrium energy should be the same as for a single phase with no miscibility gaps." res = calculate(ALFE_DBF, ['AL', 'FE'], 'LIQUID', T=[1400, 2500], P=101325, points={'LIQUID': [[0.1, 0.9], [0.2, 0.8], [0.3, 0.7], [0.7, 0.3], [0.8, 0.2]]}) eq = equilibrium(ALFE_DBF, ['AL', 'FE'], 'LIQUID', {v.T: [1400, 2500], v.P: 101325, v.X('AL'): [0.1, 0.2, 0.3, 0.7, 0.8]}, verbose=True) assert_allclose(np.squeeze(eq.GM), np.squeeze(res.GM), atol=0.1)
def compute_error(*args): prefill_callables = {key: functools.partial(*itertools.chain([func], args[:len(params)])) for key, func in callables.items()} result = calculate(dbf, data['components'], data['phases'], output=data['output'], points=np.atleast_2d(data['solver']['sublattice_configuration']).astype(np.float), callables=prefill_callables, model=fit_models, **extra_conds) # Eliminate data below 300 K for now error = (result[data['output']] - exp_values).sel(T=slice(300, None)).values.flatten() return error
def test_eq_single_phase(): "Equilibrium energy should be the same as for a single phase with no miscibility gaps." res = calculate(ALFE_DBF, ['AL', 'FE'], 'LIQUID', T=[1400, 2500], P=101325, points={'LIQUID': [[0.1, 0.9], [0.2, 0.8], [0.3, 0.7], [0.7, 0.3], [0.8, 0.2]]}) eq = equilibrium(ALFE_DBF, ['AL', 'FE'], 'LIQUID', {v.T: [1400, 2500], v.P: 101325, v.X('AL'): [0.1, 0.2, 0.3, 0.7, 0.8]}, verbose=True, pbar=False) assert_allclose(eq.GM, res.GM, atol=0.1)
def compute_tdb_energy_nc(sim, temps, c, phase): """ Computes Gibbs Free Energy and its derivative*S* w.r.t. composition, for a given temperature field and list of composition fields Derivatives are computed by holding all other explicit composition variables constant c_i is increased, c_N is decreased (the implicitly-defined last composition variable which equals 1-sum(c_i) ) Input parameters: - sim: the Simulation object. The method retrieves the TDB pycalphad object (sim._tdb) and the components used (sim._components) from this variable. - temps: the temperature array. Could also be retrieved as sim.temperature - c: the list of composition arrays. The format is a python list of numpy ndarrays - phase: the String which corresponds to a particular phase in the TDB file. E.g.: "FCC_A1" or "LIQUID" Returns GM (Molar Gibbs Free Energy) and dGdci (list of derivatives of GM, w.r.t. c_i) """ import pycalphad as pyc #alphabetical order of components! fec = [] #flattened expanded c for i in range(len(c)): fec.append(np.expand_dims(c[i].flatten(), axis=1)) fec_n_comp = np.ones(fec[0].shape) for i in range(len(c)): fec_n_comp -= fec[i] for i in range(len(c)): fec_n_comp = np.concatenate((fec_n_comp, fec[i]), axis=1) #move final component to end, maybe ill find a way to write this better in the future... fec_n_comp = np.roll(fec_n_comp, -1, axis=1) #offset composition, for computing slope of GM w.r.t. comp fec_nc_offset = [] for i in range(len(c)): fec_offset = np.zeros([len(c)+1]) fec_offset[i] = 0.0000001 fec_offset[len(c)] = -0.0000001 fec_nc_offset.append(fec_n_comp+fec_offset) flattened_t = temps.flatten() GM = pyc.calculate(sim._tdb, sim._components, phase, P=101325, T=flattened_t, points=fec_n_comp, broadcast=False).GM.values.reshape(c[0].shape) GM_derivs = [] for i in range(len(c)): GM_derivs.append((pyc.calculate(sim._tdb, sim._components, phase, P=101325, T=flattened_t, points=fec_nc_offset[i], broadcast=False).GM.values.reshape(c[0].shape)-GM)*(10000000.)) return GM, GM_derivs
def _get_interaction_predicted_values(dbf, comps, phase_name, configuration, output): mod = Model(dbf, comps, phase_name) mod.models['idmix'] = 0 # TODO: better reference state handling endpoints = endmembers_from_interaction(configuration) first_endpoint = _translate_endmember_to_array(endpoints[0], mod.ast.atoms(v.SiteFraction)) second_endpoint = _translate_endmember_to_array(endpoints[1], mod.ast.atoms(v.SiteFraction)) grid = np.linspace(0, 1, num=100) point_matrix = grid[None].T * second_endpoint + (1 - grid)[None].T * first_endpoint # TODO: Real temperature support point_matrix = point_matrix[None, None] predicted_values = calculate( dbf, comps, [phase_name], output=output, T=298.15, P=101325, points=point_matrix, model=mod)[output].values.flatten() return grid, predicted_values
def run_test(): dbf = Database('Fe-C_Fei_Brosh_2014_09.TDB') comps = ['FE', 'C', 'VA'] phases = ['FCC_A1', 'LIQUID'] conds = {v.T: 500, v.P: 101325, v.X('C'): 0.1} x0 = {'FE': 0.7, 'C': 0.3} a = 0 b = -10 * v.T slmod = SoluteTrapModel(dbf, comps, 'FCC_A1', a=a, b=b, n=1, x0=x0) # Use custom model for fcc; use default for all others models = {'FCC_A1': slmod} eq = equilibrium(dbf, comps, phases, conds, model=models) print(eq) res = calculate(dbf, comps, 'FCC_A1', T=conds[v.T], P=conds[v.P], model=models) res_nosoltrap = calculate(dbf, comps, 'FCC_A1', T=conds[v.T], P=conds[v.P]) import matplotlib.pyplot as plt plt.scatter(res.X.sel(component='C'), res.GM, c='r') plt.scatter(res.X.sel(component='C'), res_nosoltrap.GM, c='k') plt.xlabel('Mole Fraction C') plt.ylabel('Molar Gibbs Energy') plt.title('T = {} K'.format(conds[v.T])) plt.savefig('fcc_energy.png')
def compute_values(*args): prefill_callables = { key: functools.partial(*itertools.chain([func], args[:len(params)])) for key, func in callables.items() } result = calculate( dbf, data['components'], data['phases'], output=data['output'], points=np.atleast_2d( data['solver']['sublattice_configuration']).astype(np.float), callables=prefill_callables, model=fit_models, **extra_conds) return result
def run_test(): dbf = Database() dbf.elements = frozenset(["A"]) dbf.add_phase("TEST", {}, [1]) dbf.add_phase_constituents("TEST", [["A"]]) # add THETA parameters here dbf.add_parameter("THETA", "TEST", [["A"]], 0, 334.0) conds = {v.T: np.arange(1.0, 800.0, 1), v.P: 101325} res = calculate(dbf, ["A"], "TEST", T=conds[v.T], P=conds[v.P], model=EinsteinModel, output="testprop") # res_TE = calculate(dbf, ['A'], 'TEST', T=conds[v.T], P=conds[v.P], # model=EinsteinModel, output='einstein_temperature') import matplotlib.pyplot as plt plt.scatter(res["T"], res["testprop"]) plt.xlabel("Temperature (K)") plt.ylabel("Molar Heat Capacity (J/mol-K)") plt.savefig("einstein.png")
def run_test(): dbf = Database() dbf.elements = frozenset(['A']) dbf.add_phase('TEST', {}, [1]) dbf.add_phase_constituents('TEST', [['A']]) # add THETA parameters here dbf.add_parameter('THETA', 'TEST', [['A']], 0, 334.) conds = {v.T: np.arange(1.,800.,1), v.P: 101325} res = calculate(dbf, ['A'], 'TEST', T=conds[v.T], P=conds[v.P], model=EinsteinModel, output='testprop') #res_TE = calculate(dbf, ['A'], 'TEST', T=conds[v.T], P=conds[v.P], # model=EinsteinModel, output='einstein_temperature') import matplotlib.pyplot as plt plt.scatter(res['T'], res['testprop']) plt.xlabel('Temperature (K)') plt.ylabel('Molar Heat Capacity (J/mol-K)') plt.savefig('einstein.png') print(dbf.to_string(fmt='tdb'))
def test_invalid_arguments_energy_zero(): "Undefined symbols in CompiledModel are set to zero (gh-54)." TEST_TDB = """PHASE M7C3_D101 % 2 7 3 ! CONSTITUENT M7C3_D101 :MN:C: ! PARAMETER G(M7C3_D101,MN:C;0) 1 VV22+VV23*T**2+VV24*T**3 +VV25*T**4+VV26*T**5; 6000 N !""" dbf = Database(TEST_TDB) from sympy import Symbol with warnings.catch_warnings(record=True) as w: res = calculate(dbf, ['MN', 'C'], 'M7C3_D101', T=300, P=101325, parameters={Symbol('VV22'): 100}) assert res.GM.values[0, 0, 0] == 10. # 100 / 10 moles per formula-unit categories = [warning.__dict__['_category_name'] for warning in w] assert 'UserWarning' in categories assert len(w) == 4
def compute_error(*args): prefill_callables = { key: functools.partial(*itertools.chain([func], args[:len(params)])) for key, func in callables.items() } result = calculate( dbf, data['components'], data['phases'], output=data['output'], points=np.atleast_2d( data['solver']['sublattice_configuration']).astype(np.float), callables=prefill_callables, model=fit_models, **extra_conds) # Eliminate data below 300 K for now error = (result[data['output']] - exp_values).sel(T=slice(300, None)).values.flatten() return error
def residual_thermochemical(fit_params, input_data, dbf, comps, mods, callables, grad_callables): "Return an array with the residuals for thermochemical data in 'input_data'." global_comps = [x.upper() for x in sorted(comps) if x != 'VA'] param_names = fit_params.valuesdict().keys() parvalues = fit_params.valuesdict().values() # Prefill parameter values before passing to energy calculator iter_callables = {name: functools.partial(func, *parvalues) for name, func in callables.items()} iter_grad_callables = {name: functools.partial(func, *parvalues) for name, func in grad_callables.items()} res = np.zeros(len(input_data)) # TODO: This should definitely be vectorized # It will probably require an update to equilibrium() for idx, row in input_data.iterrows(): conditions = dict() if 'T' in row: conditions[v.T] = row['T'] if 'P' in row: conditions[v.P] = row['P'] for comp in global_comps[:-1]: conditions[v.X(comp)] = row['X('+comp+')'] statevars = dict((str(key), value) for key, value in conditions.items() if key in [v.T, v.P]) eq = equilibrium(dbf, comps, row['Phase'], conditions, model=mods, callables=iter_callables, grad_callables=iter_grad_callables, verbose=False) # TODO: Support for miscibility gaps, i.e., FCC_A1#1 specification eq_values = eq['Y'].sel(vertex=0).values #print(eq_values) # TODO: All the needed 'Types' should be precalculated and looked up variables = sorted(mods[row['Phase']].energy.atoms(v.StateVariable).union({v.T, v.P}), key=str) output_callables = {row['Phase']: functools.partial(make_callable(getattr(mods[row['Phase']], row['Type']), itertools.chain(param_names, variables)), *parvalues)} calculated_value = calculate(dbf, comps, row['Phase'], output=row['Type'], model=mods, callables=output_callables, points=eq_values, **statevars) res[idx] = float(row['Value']) - float(calculated_value[row['Type']].values) #print('res', idx, res[idx]) return res
def run_test(): dbf = Database() dbf.elements = frozenset(['A']) dbf.add_phase('TEST', {}, [1]) dbf.add_phase_constituents('TEST', [['A']]) # add THETA parameters here dbf.add_parameter('THETA', 'TEST', [['A']], 0, 334.) conds = {v.T: np.arange(1., 800., 1), v.P: 101325} res = calculate(dbf, ['A'], 'TEST', T=conds[v.T], P=conds[v.P], model=EinsteinModel, output='testprop') #res_TE = calculate(dbf, ['A'], 'TEST', T=conds[v.T], P=conds[v.P], # model=EinsteinModel, output='einstein_temperature') import matplotlib.pyplot as plt plt.scatter(res['T'], res['testprop']) plt.xlabel('Temperature (K)') plt.ylabel('Molar Heat Capacity (J/mol-K)') plt.savefig('einstein.png') print(dbf.to_string(fmt='tdb'))
def sample_phase_points(dbf, comps, phase_name, conditions, calc_pdens, pdens): """Sample new points from a phase around the single phase equilibrium site fractions at the given conditions. Parameters ---------- dbf : comps : phase_name : conditions : calc_pdens : The point density passed to calculate for the nominal points added. pdens : int The number of points to add in the local sampling at each set of equilibrium site fractions. Returns ------- np.ndarray[:,:] """ _, subl_dof = generate_dof(dbf.phases[phase_name], unpack_components(dbf, comps)) # subl_dof is number of species in each sublattice, e.g. (FE,NI,TI)(FE,NI)(FE,NI,TI) is [3, 2, 3] eqgrid = equilibrium(dbf, comps, [phase_name], conditions) all_eq_pts = eqgrid.Y.values[eqgrid.Phase.values == phase_name] # sample points locally additional_points = local_sample(all_eq_pts, subl_dof, pdens) # get the grid between endmembers and random point sampling from calculate pts_calc = calculate(dbf, comps, phase_name, pdens=calc_pdens, P=101325, T=300, N=1).Y.values.squeeze() return np.concatenate([additional_points, pts_calc], axis=0)
def test_statevar_upcast(): "Integer state variable values are cast to float." calculate(DBF, ['AL', 'CR', 'NI'], 'L12_FCC', T=1273, mode='numpy')
def test_surface(): "Bare minimum: calculation produces a result." calculate(DBF, ['AL', 'CR', 'NI'], 'L12_FCC', T=1273., mode='numpy')
fig = plt.figure(figsize=(9,6)) binplot(db_cuni, ['CU', 'NI', 'VA'] , my_phases_cuni, {v.X('NI'):(0,1,0.01), v.T: (1300, 1800, 5), v.P:101325}, ax=fig.gca()) plt.savefig('CuNi.png', dpi=400, bbox_inches='tight') plt.close() # It is very common in CALPHAD modeling to directly examine the Gibbs energy surface of all the constituent phases in a system. # Below we show how the Gibbs energy of all phases may be calculated as a function of composition at a given temperature (1550 K). # Calculate Energy Surfaces of Binary Systems if not os.path.isfile('CuNi_energy.png'): legend_handles, colorlist = phase_legend(my_phases_cuni) fig = plt.figure(figsize=(9,6)) ax = fig.gca() xref = np.linspace(-0.1,1.1,150) for name in my_phases_cuni: result = calculate(db_cuni, ['CU', 'NI', 'VA'], name, T=1550, output='GM') x = np.ravel(result.X.sel(component='NI')) y = np.ravel(7.124e-4*result.GM) ax.scatter(x, y, marker='.', s=5, color=colorlist[name.upper()]) popt, pcov = curve_fit(func, x, y) print name, popt ax.plot(xref, func(xref,popt[0],popt[1],popt[2],popt[3],popt[4],popt[5],popt[6],popt[7],popt[8],popt[9],popt[10]), '-', color=colorlist[name.upper()]) roo = newton(fprime, 0.5, args=(popt[0],popt[1],popt[2],popt[3],popt[4],popt[5],popt[6],popt[7],popt[8],popt[9]),maxiter=1000) print name, "Equilibrium x_Ni near ", roo ax.set_xlim((-0.1, 1.1)) ax.legend(handles=legend_handles, loc='center left', bbox_to_anchor=(1, 0.6)) plt.savefig('CuNi_energy.png',dpi=400,bbox_inches='tight') plt.close()
def test_issue116(): "Calculate gives correct result when a state variable is left as default (gh-116)." result_one = calculate(DBF, ['AL', 'CR', 'NI'], 'LIQUID', T=400) result_two = calculate(DBF, ['AL', 'CR', 'NI'], 'LIQUID', T=400, P=101325) np.testing.assert_array_equal(result_one.GM.values, result_two.GM.values)
def test_unknown_model_attribute(): "Sampling an unknown model attribute raises exception." calculate(DBF, ['AL', 'CR', 'NI'], 'L12_FCC', T=1400.0, output='_fail_')
def equilibrium(dbf, comps, phases, conditions, **kwargs): """ Calculate the equilibrium state of a system containing the specified components and phases, under the specified conditions. Model parameters are taken from 'dbf'. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : list Names of components to consider in the calculation. phases : list or dict Names of phases to consider in the calculation. conditions : dict or (list of dict) StateVariables and their corresponding value. verbose : bool, optional (Default: True) Show progress of calculations. grid_opts : dict, optional Keyword arguments to pass to the initial grid routine. Returns ------- Structured equilibrium calculation. Examples -------- None yet. """ active_phases = unpack_phases(phases) or sorted(dbf.phases.keys()) comps = sorted(comps) indep_vars = ['T', 'P'] grid_opts = kwargs.pop('grid_opts', dict()) verbose = kwargs.pop('verbose', True) phase_records = dict() callable_dict = kwargs.pop('callables', dict()) grad_callable_dict = kwargs.pop('grad_callables', dict()) hess_callable_dict = kwargs.pop('hess_callables', dict()) points_dict = dict() maximum_internal_dof = 0 conds = OrderedDict((key, unpack_condition(value)) for key, value in sorted(conditions.items(), key=str)) str_conds = OrderedDict((str(key), value) for key, value in conds.items()) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) components = [x for x in sorted(comps) if not x.startswith('VA')] # Construct models for each phase; prioritize user models models = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) if verbose: print('Components:', ' '.join(comps)) print('Phases:', end=' ') for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name) variables = sorted(mod.energy.atoms(v.StateVariable).union({key for key in conditions.keys() if key in [v.T, v.P]}), key=str) site_fracs = sorted(mod.energy.atoms(v.SiteFraction), key=str) maximum_internal_dof = max(maximum_internal_dof, len(site_fracs)) # Extra factor '1e-100...' is to work around an annoying broadcasting bug for zero gradient entries #models[name].models['_broadcaster'] = 1e-100 * Mul(*variables) ** 3 out = models[name].energy undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) callable_dict[name], grad_callable_dict[name], hess_callable_dict[name] = \ build_functions(out, [v.P, v.T] + site_fracs) # Adjust gradient by the approximate chemical potentials hyperplane = Add(*[v.MU(i)*mole_fraction(dbf.phases[name], comps, i) for i in comps if i != 'VA']) plane_obj, plane_grad, plane_hess = build_functions(hyperplane, [v.MU(i) for i in comps if i != 'VA']+site_fracs) phase_records[name.upper()] = PhaseRecord(variables=variables, grad=grad_callable_dict[name], hess=hess_callable_dict[name], plane_grad=plane_grad, plane_hess=plane_hess) if verbose: print(name, end=' ') if verbose: print('[done]', end='\n') # 'calculate' accepts conditions through its keyword arguments grid_opts.update({key: value for key, value in str_conds.items() if key in indep_vars}) if 'pdens' not in grid_opts: grid_opts['pdens'] = 100 coord_dict = str_conds.copy() coord_dict['vertex'] = np.arange(len(components)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape coord_dict['component'] = components if verbose: print('Computing initial grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties = xray.Dataset({'NP': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape)), 'GM': (list(str_conds.keys()), np.empty(grid_shape[:-1])), 'MU': (list(str_conds.keys()) + ['component'], np.empty(grid_shape)), 'points': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype=np.int)) }, coords=coord_dict, attrs={'iterations': 1}, ) # Store the potentials from the previous iteration current_potentials = properties.MU.copy() for iteration in range(MAX_ITERATIONS): if verbose: print('Computing convex hull [iteration {}]'.format(properties.attrs['iterations'])) # lower_convex_hull will modify properties lower_convex_hull(grid, properties) progress = np.abs(current_potentials - properties.MU).values converged = (progress < MIN_PROGRESS).all(axis=-1) if verbose: print('progress', progress.max(), '[{} conditions updated]'.format(np.sum(~converged))) if progress.max() < MIN_PROGRESS: if verbose: print('Convergence achieved') break current_potentials[...] = properties.MU.values if verbose: print('Refining convex hull') # Insert extra dimensions for non-T,P conditions so GM broadcasts correctly energy_broadcast_shape = grid.GM.values.shape[:len(indep_vals)] + \ (1,) * (len(str_conds) - len(indep_vals)) + (grid.GM.values.shape[-1],) driving_forces = np.einsum('...i,...i', properties.MU.values[..., np.newaxis, :].astype(np.float), grid.X.values[np.index_exp[...] + (np.newaxis,) * (len(str_conds) - len(indep_vals)) + np.index_exp[:, :]].astype(np.float)) - \ grid.GM.values.view().reshape(energy_broadcast_shape) for name in active_phases: dof = len(models[name].energy.atoms(v.SiteFraction)) current_phase_indices = (grid.Phase.values == name).reshape(energy_broadcast_shape[:-1] + (-1,)) # Broadcast to capture all conditions current_phase_indices = np.broadcast_arrays(current_phase_indices, np.empty(driving_forces.shape))[0] # This reshape is safe as long as phases have the same number of points at all indep. conditions current_phase_driving_forces = driving_forces[current_phase_indices].reshape( current_phase_indices.shape[:-1] + (-1,)) # Note: This works as long as all points are in the same phase order for all T, P current_site_fractions = grid.Y.values[..., current_phase_indices[(0,) * len(str_conds)], :] if np.sum(current_site_fractions[(0,) * len(indep_vals)][..., :dof]) == dof: # All site fractions are 1, aka zero internal degrees of freedom # Impossible to refine these points, so skip this phase points_dict[name] = current_site_fractions[(0,) * len(indep_vals)][..., :dof] continue # Find the N points with largest driving force for a given set of conditions # Remember that driving force has a sign, so we want the "most positive" values # N is the number of components, in this context # N points define a 'best simplex' for every set of conditions # We also need to restrict ourselves to one phase at a time trial_indices = np.argpartition(current_phase_driving_forces, -len(components), axis=-1)[..., -len(components):] trial_indices = trial_indices.ravel() statevar_indices = np.unravel_index(np.arange(np.multiply.reduce(properties.GM.values.shape + (len(components),))), properties.GM.values.shape + (len(components),))[:len(indep_vals)] points = current_site_fractions[np.index_exp[statevar_indices + (trial_indices,)]] points.shape = properties.points.shape[:-1] + (-1, maximum_internal_dof) # The Y arrays have been padded, so we should slice off the padding points = points[..., :dof] #print('Starting points shape: ', points.shape) #print(points) if len(points) == 0: if name in points_dict: del points_dict[name] # No nearly stable points: skip this phase continue num_vars = len(phase_records[name].variables) plane_grad = phase_records[name].plane_grad plane_hess = phase_records[name].plane_hess statevar_grid = np.meshgrid(*itertools.chain(indep_vals), sparse=True, indexing='ij') # TODO: A more sophisticated treatment of constraints num_constraints = len(dbf.phases[name].sublattices) constraint_jac = np.zeros((num_constraints, num_vars-len(indep_vars))) # Independent variables are always fixed (in this limited implementation) #for idx in range(len(indep_vals)): # constraint_jac[idx, idx] = 1 # This is for site fraction balance constraints var_idx = 0#len(indep_vals) for idx in range(len(dbf.phases[name].sublattices)): active_in_subl = set(dbf.phases[name].constituents[idx]).intersection(comps) constraint_jac[idx, var_idx:var_idx + len(active_in_subl)] = 1 var_idx += len(active_in_subl) newton_iteration = 0 while newton_iteration < MAX_NEWTON_ITERATIONS: flattened_points = points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])) grad_args = itertools.chain([i[..., None] for i in statevar_grid], [flattened_points[..., i] for i in range(flattened_points.shape[-1])]) grad = np.array(phase_records[name].grad(*grad_args), dtype=np.float) # Remove derivatives wrt T,P grad = grad[..., len(indep_vars):] grad.shape = points.shape grad[np.isnan(grad).any(axis=-1)] = 0 # This is necessary for gradients on the edge of space hess_args = itertools.chain([i[..., None] for i in statevar_grid], [flattened_points[..., i] for i in range(flattened_points.shape[-1])]) hess = np.array(phase_records[name].hess(*hess_args), dtype=np.float) # Remove derivatives wrt T,P hess = hess[..., len(indep_vars):, len(indep_vars):] hess.shape = points.shape + (hess.shape[-1],) hess[np.isnan(hess).any(axis=(-2, -1))] = np.eye(hess.shape[-1]) plane_args = itertools.chain([properties.MU.values[..., i][..., None] for i in range(properties.MU.shape[-1])], [points[..., i] for i in range(points.shape[-1])]) cast_grad = np.array(plane_grad(*plane_args), dtype=np.float) # Remove derivatives wrt chemical potentials cast_grad = cast_grad[..., properties.MU.shape[-1]:] grad = grad - cast_grad plane_args = itertools.chain([properties.MU.values[..., i][..., None] for i in range(properties.MU.shape[-1])], [points[..., i] for i in range(points.shape[-1])]) cast_hess = np.array(plane_hess(*plane_args), dtype=np.float) # Remove derivatives wrt chemical potentials cast_hess = cast_hess[..., properties.MU.shape[-1]:, properties.MU.shape[-1]:] cast_hess = -cast_hess + hess hess = cast_hess.astype(np.float, copy=False) try: e_matrix = np.linalg.inv(hess) except np.linalg.LinAlgError: print(hess) raise current = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])), **grid_opts) current_plane = np.multiply(current.X.values.reshape(points.shape[:-1] + (len(components),)), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) current_df = current.GM.values.reshape(points.shape[:-1]) - current_plane #print('Inv hess check: ', np.isnan(e_matrix).any()) #print('grad check: ', np.isnan(grad).any()) dy_unconstrained = -np.einsum('...ij,...j->...i', e_matrix, grad) #print('dy_unconstrained check: ', np.isnan(dy_unconstrained).any()) proj_matrix = np.dot(e_matrix, constraint_jac.T) inv_matrix = np.rollaxis(np.dot(constraint_jac, proj_matrix), 0, -1) inv_term = np.linalg.inv(inv_matrix) #print('inv_term check: ', np.isnan(inv_term).any()) first_term = np.einsum('...ij,...jk->...ik', proj_matrix, inv_term) #print('first_term check: ', np.isnan(first_term).any()) # Normally a term for the residual here # We only choose starting points which obey the constraints, so r = 0 cons_summation = np.einsum('...i,...ji->...j', dy_unconstrained, constraint_jac) #print('cons_summation check: ', np.isnan(cons_summation).any()) cons_correction = np.einsum('...ij,...j->...i', first_term, cons_summation) #print('cons_correction check: ', np.isnan(cons_correction).any()) dy_constrained = dy_unconstrained - cons_correction #print('dy_constrained check: ', np.isnan(dy_constrained).any()) # TODO: Support for adaptive changing independent variable steps new_direction = dy_constrained #print('new_direction', new_direction) #print('points', points) # Backtracking line search if np.isnan(new_direction).any(): print('new_direction', new_direction) #print('Convergence angle:', -(grad*new_direction).sum(axis=-1) / (np.linalg.norm(grad, axis=-1) * np.linalg.norm(new_direction, axis=-1))) new_points = points + INITIAL_STEP_SIZE * new_direction alpha = np.full(new_points.shape[:-1], INITIAL_STEP_SIZE, dtype=np.float) alpha[np.all(np.linalg.norm(new_direction, axis=-1) < MIN_DIRECTION_NORM, axis=-1)] = 0 negative_points = np.any(new_points < 0., axis=-1) while np.any(negative_points): alpha[negative_points] *= 0.5 new_points = points + alpha[..., np.newaxis] * new_direction negative_points = np.any(new_points < 0., axis=-1) # Backtracking line search # alpha now contains maximum possible values that keep us inside the space # but we don't just want to take the biggest step; we want the biggest step which reduces energy new_points = new_points.reshape(new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) candidates = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=new_points, **grid_opts) candidate_plane = np.multiply(candidates.X.values.reshape(points.shape[:-1] + (len(components),)), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) energy_diff = (candidates.GM.values.reshape(new_direction.shape[:-1]) - candidate_plane) - current_df new_points.shape = new_direction.shape bad_steps = energy_diff > alpha * 1e-4 * (new_direction * grad).sum(axis=-1) backtracking_iterations = 0 while np.any(bad_steps): alpha[bad_steps] *= 0.5 new_points = points + alpha[..., np.newaxis] * new_direction #print('new_points', new_points) #print('bad_steps', bad_steps) new_points = new_points.reshape(new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) candidates = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=new_points, **grid_opts) candidate_plane = np.multiply(candidates.X.values.reshape(points.shape[:-1] + (len(components),)), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) energy_diff = (candidates.GM.values.reshape(new_direction.shape[:-1]) - candidate_plane) - current_df #print('energy_diff', energy_diff) new_points.shape = new_direction.shape bad_steps = energy_diff > alpha * 1e-4 * (new_direction * grad).sum(axis=-1) backtracking_iterations += 1 if backtracking_iterations > MAX_BACKTRACKING: break biggest_step = np.max(np.linalg.norm(new_points - points, axis=-1)) if biggest_step < 1e-2: if verbose: print('N-R convergence on mini-iteration', newton_iteration, '[{}]'.format(name)) points = new_points break if verbose: #print('Biggest step:', biggest_step) #print('points', points) #print('grad of points', grad) #print('new_direction', new_direction) #print('alpha', alpha) #print('new_points', new_points) pass points = new_points newton_iteration += 1 new_points = points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])) new_points = np.concatenate((current_site_fractions[..., :dof], new_points), axis=-2) points_dict[name] = new_points if verbose: print('Rebuilding grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, points=points_dict, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties.attrs['iterations'] += 1 # One last call to ensure 'properties' and 'grid' are consistent with one another lower_convex_hull(grid, properties) ravelled_X_view = grid['X'].values.view().reshape(-1, grid['X'].values.shape[-1]) ravelled_Y_view = grid['Y'].values.view().reshape(-1, grid['Y'].values.shape[-1]) ravelled_Phase_view = grid['Phase'].values.view().reshape(-1) # Copy final point values from the grid and drop the index array # For some reason direct construction doesn't work. We have to create empty and then assign. properties['X'] = xray.DataArray(np.empty_like(ravelled_X_view[properties['points'].values]), dims=properties['points'].dims + ('component',)) properties['X'].values[...] = ravelled_X_view[properties['points'].values] properties['Y'] = xray.DataArray(np.empty_like(ravelled_Y_view[properties['points'].values]), dims=properties['points'].dims + ('internal_dof',)) properties['Y'].values[...] = ravelled_Y_view[properties['points'].values] # TODO: What about invariant reactions? We should perform a final driving force calculation here. # We can handle that in the same post-processing step where we identify single-phase regions. properties['Phase'] = xray.DataArray(np.empty_like(ravelled_Phase_view[properties['points'].values]), dims=properties['points'].dims) properties['Phase'].values[...] = ravelled_Phase_view[properties['points'].values] del properties['points'] return properties
def test_incompatible_model_instance_raises(): "Calculate raises when an incompatible Model instance built with a different phase is passed." comps = ['AL', 'CR', 'NI'] phase_name = 'L12_FCC' mod = Model(DBF, comps, 'LIQUID') # Model instance does not match the phase calculate(DBF, comps, phase_name, T=1400.0, output='_fail_', model=mod)
def test_calculate_some_phases_filtered(): """ Phases are filtered out from calculate() when some cannot be built. """ # should not raise; AL13FE4 should be filtered out calculate(ALFE_DBF, ['AL', 'VA'], ['FCC_A1', 'AL13FE4'], T=1200, P=101325)
def test_points_kwarg_multi_phase(): "Multi-phase calculation works when internal dof differ (gh-41)." calculate(DBF, ['AL', 'CR', 'NI'], ['L12_FCC', 'LIQUID'], T=1273, points={'L12_FCC': [0.20, 0.05, 0.75, 0.05, 0.20, 0.75]}, mode='numpy')
def plot_property(dbf, comps, phaseL, params, T, prop, config=None, datasets=None, xlim=None, xlabel=None, ylabel=None, yscale=None, phase_label_dict=None, unit='kJ/mol.', cdict=None, figsize=None): """ Plot a property of interest versus temperature with uncertainty bounds for all phases of interest Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters comps : list Names of components to consider in the calculation phaseL : list Names of phases to plot properties for params : numpy array Array where the rows contain the parameter sets for the pycalphad equilibrium calculation T : list, array or x-array object Temperature values at which to plot the selected property prop : str property (or attribute in pycalphad terminology) to sample, e.g. GM for molar gibbs energy or H_MIX for the enthalpy of mixing config : tuple, optional Sublattice configuration as a tuple, e.g. (“CU”, (“CU”, “MG”)) datasets : espei.utils.PickleableTinyDB, optional Database of datasets to search for data xlims : list or tuple of float, optional List or tuple with two floats corresponding to the minimum and maximum molar composition of comp xlabel : str, optional plot x label ylabel : str, optional plot y label yscale : int or float, optional scaling factor to apply to property (e.g. to plot kJ/mol. instead of J/mol. choose yscale to be 0.001) phase_label_dict : dict, optional Dictionary with keys given by phase names and corresponding strings to use in plotting (e.g. to enable LaTeX labels) unit : str, optional Unit to plot on the y-axis for the property of interest cdict : dict, optional Dictionary with phase names and corresponding colors figsize : tuple or list of int or float, optional Plot dimensions in inches Returns ------- Examples -------- >>> import numpy as np >>> import pduq.uq_plot as uq >>> from pycalphad import Database >>> dbf = Database('CU-MG_param_gen.tdb') >>> comps = ['MG', 'CU', 'VA'] >>> phaseL = ['CUMG2', 'LIQUID'] >>> params = np.loadtxt('params.npy')[: -1, :] >>> T = 650 >>> prop = 'GM' >>> # Plot the molar gibbs energy of all phases in phaseL >>> # versus molar fraction of MG at 650K. This will have >>> # uncertainty intervals generated by the parameter sets >>> # in params >>> uq.plot_property(dbf, comps, phaseL, params, T, prop) """ symbols_to_fit = database_symbols_to_fit(dbf) CI = 95 nph = len(phaseL) colorL = sns.color_palette("cubehelix", nph) markerL = 10 * [ 'o', 'D', '^', 'x', 'h', 's', 'v', '*', 'P', 'p', '>', 'd', '<' ] plt.figure(figsize=figsize) # compute uncertainty in property for each phase in list for ii in range(nph): phase = phaseL[ii] print('starting', prop, 'evaluations for the', phase, 'phase') # for each parameter sample calculate the property # for each possible site occupancy ratios compL = [] for index in range(params.shape[0]): param_dict = { param_name: param for param_name, param in zip(symbols_to_fit, params[index, :]) } parameters = OrderedDict(sorted(param_dict.items(), key=str)) comp = calculate(dbf, comps, phase, P=101325, T=T, output=prop, parameters=parameters) compL += [comp] # concatenate the calculate results in an xarray along # an axis named 'sample' compC = xr.concat(compL, 'sample') compC.coords['sample'] = np.arange(params.shape[0]) # The composition vector is the same for all samples if hasattr(T, "__len__"): Xvals = T else: Xvals = comp.X.sel(component=comps[0]).values.squeeze() Pvals = compC[prop].where(compC.Phase == phase).values.squeeze() if np.array(Xvals).size == 1: print('phase is a line compound') Xvals_ = np.array([Xvals - 0.002, Xvals + 0.002]) Pvals_ = np.vstack([Pvals, Pvals]).T else: # find the lower hull of the property by finding # the configuration with the lowest value within # each interval. In each interval record the composition # and property indxL = np.array([]) # Xbnds = np.arange(0, 1.01, 0.01) Xbnds = np.linspace(Xvals.min(), Xvals.max(), 100) for lb, ub in zip(Xbnds[:-1], Xbnds[1:]): # print('lb: ', lb, ', ub: ', ub) boolA = (lb <= Xvals) * (Xvals < ub) if boolA.sum() == 0: continue indxA = np.arange(boolA.size)[boolA] P_ = Pvals[0, boolA] indxL = np.append(indxL, indxA[P_.argmin()]) # indxL = np.append(indxL, indxA[P_.argmax()]) indxL = indxL.astype('int32') if indxL.size == 1: print('only one point found') Xvals_ = Xvals[np.asscalar(indxL)] Pvals_ = Pvals[:, np.asscalar(indxL)] else: Xvals_ = Xvals[indxL] Pvals_ = Pvals[:, indxL] # Xvals_ = Xvals # Pvals_ = Pvals # for ii in range(params.shape[0]): # plt.plot(Xvals_, Pvals_[ii, :], 'k-', linewidth=0.5, alpha=0.1) # plt.show() if yscale is not None: Pvals_ *= yscale low, mid, high = np.percentile( Pvals_, [0.5 * (100 - CI), 50, 100 - 0.5 * (100 - CI)], axis=0) if cdict is not None: color = cdict[phase] else: color = colorL[ii] if phase_label_dict is not None: label = phase_label_dict[phase] else: label = phase plt.plot(Xvals_, mid, linestyle='-', color=color, label=label) plt.fill_between(np.atleast_1d(Xvals_), low, high, alpha=0.3, facecolor=color) # collect and plot experimental data if config is not None and datasets is not None: symmetry = None data = get_data(comps, phase, config, symmetry, datasets, prop) print(data) for data_s, marker in zip(data, markerL): occupancies = data_s['solver']['sublattice_occupancies'] # at the moment this needs to be changed manually X_vec = [row[0][0] for row in occupancies] values = np.squeeze(data_s['values']) if yscale is not None: values *= yscale plt.plot(X_vec, values, linestyle='', marker=marker, markerfacecolor='none', markeredgecolor=color, markersize=6, alpha=0.9, label=data_s['reference']) if xlim is None: plt.xlim([Xvals_.min(), Xvals_.max()]) else: plt.xlim(xlim) if xlabel is not None: plt.xlabel(xlabel) else: plt.xlabel(r'$X_{%s}$' % comps[0]) if ylabel is not None: plt.ylabel(ylabel) else: plt.ylabel(prop + ' (' + unit + ')') plt.legend() plt.tight_layout()
def time_calculate_non_magnetic(self): calculate(self.db, ['AL', 'FE', 'VA'], 'LIQUID', T=(300, 2000, 10))
def calculate_thermochemical_error(dbf, comps, thermochemical_data, parameters=None): """ Calculate the weighted single phase error in the Database Parameters ---------- dbf : pycalphad.Database Database to consider comps : list List of active component names thermochemical_data : list List of thermochemical data dicts parameters : dict Dictionary of symbols that will be overridden in pycalphad.calculate Returns ------- float A single float of the residual sum of square errors Notes ----- There are different single phase values, HM_MIX, SM_FORM, CP_FORM, etc. Each of these have different units and the error cannot be compared directly. To normalize all of the errors, a normalization factor must be used. Equation 2.59 and 2.60 in Lukas, Fries, and Sundman "Computational Thermodynamics" shows how this can be considered. Each type of error will be weighted by the reciprocal of the estimated uncertainty in the measured value and conditions. The weighting factor is calculated by $ p_i = (\Delta L_i)^{-1} $ where $ \Delta L_i $ is the uncertainty in the measurement. We will neglect the uncertainty for quantities such as temperature, assuming they are small. """ if parameters is None: parameters = {} prob_error = 0.0 for data in thermochemical_data: phase_name = data['phase_name'] prop = data['prop'] calculate_dict = deepcopy(data['calculate_dict']) output = data['output'] callables = data.get('callables', None) mod = data['model'] std_devs = data['weights'] sample_values = calculate_dict.pop('values') dataset_refs = calculate_dict.pop('references') results = calculate(dbf, comps, phase_name, broadcast=False, parameters=parameters, model=mod, callables=callables, output=output, **calculate_dict)[output].values probabilities = [] differences = [] for result, sample_value, std_dev, ref in zip(results, sample_values, std_devs, dataset_refs): differences.append(result - sample_value) probabilities.append( norm(loc=0, scale=std_dev).logpdf(result - sample_value)) logging.debug( 'Thermochemical error - data: {}, differences: {}, probabilities: {}, references: {}' .format(sample_values, differences, probabilities, dataset_refs)) prob_error += np.sum(probabilities) return prob_error
def _compare_data_to_parameters(dbf, comps, phase_name, desired_data, mod, configuration, x, y, ax=None): """ Return one set of plotted Axes with data compared to calculated parameters Parameters ---------- dbf : Database pycalphad thermodynamic database containing the relevant parameters. comps : list Names of components to consider in the calculation. phase_name : str Name of the considered phase phase desired_data : mod : Model A pycalphad Model. The Model may or may not have the reference state zeroed out for formation properties. configuration : x : str Model property to plot on the x-axis e.g. 'T', 'HM_MIX', 'SM_FORM' y : str Model property to plot on the y-axis e.g. 'T', 'HM_MIX', 'SM_FORM' ax : matplotlib.Axes Default axes used if not specified. Returns ------- matplotlib.Axes """ all_samples = np.array(get_samples(desired_data), dtype=np.object) endpoints = endmembers_from_interaction(configuration) interacting_subls = [ c for c in list_to_tuple(configuration) if isinstance(c, tuple) ] disordered_config = False if (len(set(interacting_subls)) == 1) and (len(interacting_subls[0]) == 2): # This configuration describes all sublattices with the same two elements interacting # In general this is a high-dimensional space; just plot the diagonal to see the disordered mixing endpoints = [endpoints[0], endpoints[-1]] disordered_config = True if not ax: fig = plt.figure(figsize=plt.figaspect(1)) ax = fig.gca() bar_chart = False bar_labels = [] bar_data = [] if y.endswith('_FORM'): # We were passed a Model object with zeroed out reference states yattr = y[:-5] else: yattr = y if len(endpoints) == 1: # This is an endmember so we can just compute T-dependent stuff temperatures = np.array([i[0] for i in all_samples], dtype=np.float) if temperatures.min() != temperatures.max(): temperatures = np.linspace(temperatures.min(), temperatures.max(), num=100) else: # We only have one temperature: let's do a bar chart instead bar_chart = True temperatures = temperatures.min() endmember = _translate_endmember_to_array( endpoints[0], mod.ast.atoms(v.SiteFraction))[None, None] predicted_quantities = calculate(dbf, comps, [phase_name], output=yattr, T=temperatures, P=101325, points=endmember, model=mod, mode='numpy') if y == 'HM' and x == 'T': # Shift enthalpy data so that value at minimum T is zero predicted_quantities[yattr] -= predicted_quantities[yattr].sel( T=temperatures[0]).values.flatten() response_data = predicted_quantities[yattr].values.flatten() if not bar_chart: extra_kwargs = {} if len(response_data) < 10: extra_kwargs['markersize'] = 20 extra_kwargs['marker'] = '.' extra_kwargs['linestyle'] = 'none' extra_kwargs['clip_on'] = False ax.plot(temperatures, response_data, label='This work', color='k', **extra_kwargs) ax.set_xlabel(plot_mapping.get(x, x)) ax.set_ylabel(plot_mapping.get(y, y)) else: bar_labels.append('This work') bar_data.append(response_data[0]) elif len(endpoints) == 2: # Binary interaction parameter first_endpoint = _translate_endmember_to_array( endpoints[0], mod.ast.atoms(v.SiteFraction)) second_endpoint = _translate_endmember_to_array( endpoints[1], mod.ast.atoms(v.SiteFraction)) point_matrix = np.linspace(0, 1, num=100)[None].T * second_endpoint + \ (1 - np.linspace(0, 1, num=100))[None].T * first_endpoint # TODO: Real temperature support point_matrix = point_matrix[None, None] predicted_quantities = calculate(dbf, comps, [phase_name], output=yattr, T=300, P=101325, points=point_matrix, model=mod, mode='numpy') response_data = predicted_quantities[yattr].values.flatten() if not bar_chart: extra_kwargs = {} if len(response_data) < 10: extra_kwargs['markersize'] = 20 extra_kwargs['marker'] = '.' extra_kwargs['linestyle'] = 'none' extra_kwargs['clip_on'] = False ax.plot(np.linspace(0, 1, num=100), response_data, label='This work', color='k', **extra_kwargs) ax.set_xlim((0, 1)) ax.set_xlabel( str(':'.join(endpoints[0])) + ' to ' + str(':'.join(endpoints[1]))) ax.set_ylabel(plot_mapping.get(y, y)) else: bar_labels.append('This work') bar_data.append(response_data[0]) else: raise NotImplementedError( 'No support for plotting configuration {}'.format(configuration)) bib_reference_keys = sorted( list({entry['reference'] for entry in desired_data})) symbol_map = bib_marker_map(bib_reference_keys) for data in desired_data: indep_var_data = None response_data = np.zeros_like(data['values'], dtype=np.float) if x == 'T' or x == 'P': indep_var_data = np.array(data['conditions'][x], dtype=np.float).flatten() elif x == 'Z': if disordered_config: # Take the second element of the first interacting sublattice as the coordinate # Because it's disordered all sublattices should be equivalent # TODO: Fix this to filter because we need to guarantee the plot points are disordered occ = data['solver']['sublattice_occupancies'] subl_idx = np.nonzero( [isinstance(c, (list, tuple)) for c in occ[0]])[0] if len(subl_idx) > 1: subl_idx = int(subl_idx[0]) else: subl_idx = int(subl_idx) indep_var_data = [c[subl_idx][1] for c in occ] else: interactions = np.array([i[1][1] for i in get_samples([data])], dtype=np.float) indep_var_data = 1 - (interactions + 1) / 2 if y.endswith('_MIX') and data['output'].endswith('_FORM'): # All the _FORM data we have still has the lattice stability contribution # Need to zero it out to shift formation data to mixing mod_latticeonly = Model( dbf, comps, phase_name, parameters={'GHSER' + c.upper(): 0 for c in comps}) mod_latticeonly.models = { key: value for key, value in mod_latticeonly.models.items() if key == 'ref' } temps = data['conditions'].get('T', 300) pressures = data['conditions'].get('P', 101325) points = build_sitefractions( phase_name, data['solver']['sublattice_configurations'], data['solver']['sublattice_occupancies']) for point_idx in range(len(points)): missing_variables = mod_latticeonly.ast.atoms( v.SiteFraction) - set(points[point_idx].keys()) # Set unoccupied values to zero points[point_idx].update( {key: 0 for key in missing_variables}) # Change entry to a sorted array of site fractions points[point_idx] = list( OrderedDict(sorted(points[point_idx].items(), key=str)).values()) points = np.array(points, dtype=np.float) # TODO: Real temperature support points = points[None, None] stability = calculate(dbf, comps, [phase_name], output=data['output'][:-5], T=temps, P=pressures, points=points, model=mod_latticeonly, mode='numpy') response_data -= stability[data['output'][:-5]].values response_data += np.array(data['values'], dtype=np.float) response_data = response_data.flatten() if not bar_chart: extra_kwargs = {} if len(response_data) < 10: extra_kwargs['markersize'] = 8 extra_kwargs['linestyle'] = 'none' extra_kwargs['clip_on'] = False ref = data.get('reference', '') mark = symbol_map[ref]['markers'] ax.plot(indep_var_data, response_data, label=symbol_map[ref]['formatted'], marker=mark['marker'], fillstyle=mark['fillstyle'], **extra_kwargs) else: bar_labels.append(data.get('reference', None)) bar_data.append(response_data[0]) if bar_chart: ax.barh(0.02 * np.arange(len(bar_data)), bar_data, color='k', height=0.01) endmember_title = ' to '.join([':'.join(i) for i in endpoints]) ax.get_figure().suptitle('{} (T = {} K)'.format( endmember_title, temperatures), fontsize=20) ax.set_yticks(0.02 * np.arange(len(bar_data))) ax.set_yticklabels(bar_labels, fontsize=20) # This bar chart is rotated 90 degrees, so "y" is now x ax.set_xlabel(plot_mapping.get(y, y)) else: ax.set_frame_on(False) leg = ax.legend(loc='best') leg.get_frame().set_edgecolor('black') return ax
def plot_results(input_database, datasets, params, databases=None): """ Generate figures using the datasets and trace of the parameters. A dict of label->Database objects may be provided as a kwarg. """ # Add extra broadcast dimensions for T, P, and 'points' param_tr = [i[None, None, None].T for i in params.values()] def plot_key(obj): plot = obj.json.get('plot', None) return (plot['x'], plot['y']) if plot else None datasets = sorted(datasets, key=plot_key) databases = dict() if databases is None else databases for plot_data_type, data_group in itertools.groupby(datasets, key=plot_key): if plot_data_type is None: continue figure = plt.figure(figsize=(15, 12)) data_group = list(data_group) x, y = plot_data_type # All of data_group should be calculating the same thing... # Don't show fits below 300 K since they're currently meaningless # TODO: Calls to flatten() should actually be slicing operations # We can get away with it for now since all datasets will be 2D fit = data_group[0].calc_func(*param_tr).sel(T=slice(300, None)) mu = fit[y].values.mean(axis=0).flatten() sigma = 2 * fit[y].values.std(axis=0).flatten() figure.gca().plot(fit[x].values.flatten(), mu, '-k', label='This work') figure.gca().fill_between(fit[x].values.flatten(), mu - sigma, mu + sigma, color='lightgray') for data in data_group: plot_label = data.json['plot'].get('name', None) figure.gca().plot(data.exp_data[x].values, data.exp_data.values.flatten(), label=plot_label) for label, dbf in databases.items(): # TODO: Relax this restriction if data_group[0].json['solver']['mode'] != 'manual': continue conds = data_group[0].json['conditions'] conds['T'] = np.array(conds['T']) conds['T'] = conds['T'][conds['T'] >= 300.] for key in conds.keys(): if key not in ['T', 'P']: raise ValueError('Invalid conditions in JSON file') # To work around differences in sublattice models, relax the internal dof global_comps = sorted(set(data_group[0].json['components']) - set(['VA'])) compositions = \ _map_internal_dof(input_database, sorted(data_group[0].json['components']), data_group[0].json['phases'][0], np.atleast_2d( data_group[0].json['solver']['sublattice_configuration']).astype(np.float)) # Tiny perturbation to work around a bug in lower_convex_hull (gh-28) compare_conds = {v.X(comp): np.add(compositions[:, idx], 1e-4).flatten().tolist() for idx, comp in enumerate(global_comps[:-1])} compare_conds.update({v.__dict__[key]: value for key, value in conds.items()}) # We only want to relax the internal dof at the lowest temperature # This will help us capture the most related sublattice config since solver mode=manual # probably means this is first-principles data compare_conds[v.T] = 300. eqres = equilibrium(dbf, data_group[0].json['components'], str(data_group[0].json['phases'][0]), compare_conds, verbose=False) internal_dof = sum(map(len, dbf.phases[data_group[0].json['phases'][0]].constituents)) largest_phase_fraction = eqres['NP'].values.argmax() eqpoints = eqres['Y'].values[..., largest_phase_fraction, :internal_dof] result = calculate(dbf, data_group[0].json['components'], str(data_group[0].json['phases'][0]), output=y, points=eqpoints, **conds) # Don't show CALPHAD results below 300 K because they're meaningless right now result = result.sel(T=slice(300, None)) figure.gca().plot(result[x].values.flatten(), result[y].values.flatten(), label=label) label_mapping = dict(x=x, y=y) label_mapping['CPM'] = 'Molar Heat Capacity (J/mol-atom-K)' label_mapping['SM'] = 'Molar Entropy (J/mol-atom-K)' label_mapping['HM'] = 'Molar Enthalpy (J/mol-atom)' label_mapping['T'] = 'Temperature (K)' label_mapping['P'] = 'Pressure (Pa)' figure.gca().set_xlabel(label_mapping[x], fontsize=20) figure.gca().set_ylabel(label_mapping[y], fontsize=20) figure.gca().tick_params(axis='both', which='major', labelsize=20) figure.gca().legend(loc='best', fontsize=16) figure.canvas.draw() yield figure plt.show()
def equilibrium(dbf, comps, phases, conditions, **kwargs): """ Calculate the equilibrium state of a system containing the specified components and phases, under the specified conditions. Model parameters are taken from 'dbf'. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : list Names of components to consider in the calculation. phases : list or dict Names of phases to consider in the calculation. conditions : dict or (list of dict) StateVariables and their corresponding value. verbose : bool, optional (Default: True) Show progress of calculations. grid_opts : dict, optional Keyword arguments to pass to the initial grid routine. Returns ------- Structured equilibrium calculation. Examples -------- None yet. """ active_phases = unpack_phases(phases) or sorted(dbf.phases.keys()) comps = sorted(comps) indep_vars = ['T', 'P'] grid_opts = kwargs.pop('grid_opts', dict()) verbose = kwargs.pop('verbose', True) phase_records = dict() callable_dict = kwargs.pop('callables', dict()) grad_callable_dict = kwargs.pop('grad_callables', dict()) hess_callable_dict = kwargs.pop('hess_callables', dict()) points_dict = dict() maximum_internal_dof = 0 conds = OrderedDict((key, unpack_condition(value)) for key, value in sorted(conditions.items(), key=str)) str_conds = OrderedDict((str(key), value) for key, value in conds.items()) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) components = [x for x in sorted(comps) if not x.startswith('VA')] # Construct models for each phase; prioritize user models models = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) if verbose: print('Components:', ' '.join(comps)) print('Phases:', end=' ') for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name) variables = sorted(mod.energy.atoms(v.StateVariable).union( {key for key in conditions.keys() if key in [v.T, v.P]}), key=str) site_fracs = sorted(mod.energy.atoms(v.SiteFraction), key=str) maximum_internal_dof = max(maximum_internal_dof, len(site_fracs)) # Extra factor '1e-100...' is to work around an annoying broadcasting bug for zero gradient entries #models[name].models['_broadcaster'] = 1e-100 * Mul(*variables) ** 3 out = models[name].energy undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) callable_dict[name], grad_callable_dict[name], hess_callable_dict[name] = \ build_functions(out, [v.P, v.T] + site_fracs) # Adjust gradient by the approximate chemical potentials hyperplane = Add(*[ v.MU(i) * mole_fraction(dbf.phases[name], comps, i) for i in comps if i != 'VA' ]) plane_obj, plane_grad, plane_hess = build_functions( hyperplane, [v.MU(i) for i in comps if i != 'VA'] + site_fracs) phase_records[name.upper()] = PhaseRecord( variables=variables, grad=grad_callable_dict[name], hess=hess_callable_dict[name], plane_grad=plane_grad, plane_hess=plane_hess) if verbose: print(name, end=' ') if verbose: print('[done]', end='\n') # 'calculate' accepts conditions through its keyword arguments grid_opts.update( {key: value for key, value in str_conds.items() if key in indep_vars}) if 'pdens' not in grid_opts: grid_opts['pdens'] = 100 coord_dict = str_conds.copy() coord_dict['vertex'] = np.arange(len(components)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape coord_dict['component'] = components if verbose: print('Computing initial grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties = xray.Dataset( { 'NP': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape)), 'GM': (list(str_conds.keys()), np.empty(grid_shape[:-1])), 'MU': (list(str_conds.keys()) + ['component'], np.empty(grid_shape)), 'points': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype=np.int)) }, coords=coord_dict, attrs={'iterations': 1}, ) # Store the potentials from the previous iteration current_potentials = properties.MU.copy() for iteration in range(MAX_ITERATIONS): if verbose: print('Computing convex hull [iteration {}]'.format( properties.attrs['iterations'])) # lower_convex_hull will modify properties lower_convex_hull(grid, properties) progress = np.abs(current_potentials - properties.MU).values converged = (progress < MIN_PROGRESS).all(axis=-1) if verbose: print('progress', progress.max(), '[{} conditions updated]'.format(np.sum(~converged))) if progress.max() < MIN_PROGRESS: if verbose: print('Convergence achieved') break current_potentials[...] = properties.MU.values if verbose: print('Refining convex hull') # Insert extra dimensions for non-T,P conditions so GM broadcasts correctly energy_broadcast_shape = grid.GM.values.shape[:len(indep_vals)] + \ (1,) * (len(str_conds) - len(indep_vals)) + (grid.GM.values.shape[-1],) driving_forces = np.einsum('...i,...i', properties.MU.values[..., np.newaxis, :].astype(np.float), grid.X.values[np.index_exp[...] + (np.newaxis,) * (len(str_conds) - len(indep_vals)) + np.index_exp[:, :]].astype(np.float)) - \ grid.GM.values.view().reshape(energy_broadcast_shape) for name in active_phases: dof = len(models[name].energy.atoms(v.SiteFraction)) current_phase_indices = (grid.Phase.values == name ).reshape(energy_broadcast_shape[:-1] + (-1, )) # Broadcast to capture all conditions current_phase_indices = np.broadcast_arrays( current_phase_indices, np.empty(driving_forces.shape))[0] # This reshape is safe as long as phases have the same number of points at all indep. conditions current_phase_driving_forces = driving_forces[ current_phase_indices].reshape( current_phase_indices.shape[:-1] + (-1, )) # Note: This works as long as all points are in the same phase order for all T, P current_site_fractions = grid.Y.values[..., current_phase_indices[ (0, ) * len(str_conds)], :] if np.sum( current_site_fractions[(0, ) * len(indep_vals)][..., :dof]) == dof: # All site fractions are 1, aka zero internal degrees of freedom # Impossible to refine these points, so skip this phase points_dict[name] = current_site_fractions[ (0, ) * len(indep_vals)][..., :dof] continue # Find the N points with largest driving force for a given set of conditions # Remember that driving force has a sign, so we want the "most positive" values # N is the number of components, in this context # N points define a 'best simplex' for every set of conditions # We also need to restrict ourselves to one phase at a time trial_indices = np.argpartition(current_phase_driving_forces, -len(components), axis=-1)[..., -len(components):] trial_indices = trial_indices.ravel() statevar_indices = np.unravel_index( np.arange( np.multiply.reduce(properties.GM.values.shape + (len(components), ))), properties.GM.values.shape + (len(components), ))[:len(indep_vals)] points = current_site_fractions[np.index_exp[statevar_indices + (trial_indices, )]] points.shape = properties.points.shape[:-1] + ( -1, maximum_internal_dof) # The Y arrays have been padded, so we should slice off the padding points = points[..., :dof] #print('Starting points shape: ', points.shape) #print(points) if len(points) == 0: if name in points_dict: del points_dict[name] # No nearly stable points: skip this phase continue num_vars = len(phase_records[name].variables) plane_grad = phase_records[name].plane_grad plane_hess = phase_records[name].plane_hess statevar_grid = np.meshgrid(*itertools.chain(indep_vals), sparse=True, indexing='ij') # TODO: A more sophisticated treatment of constraints num_constraints = len(dbf.phases[name].sublattices) constraint_jac = np.zeros( (num_constraints, num_vars - len(indep_vars))) # Independent variables are always fixed (in this limited implementation) #for idx in range(len(indep_vals)): # constraint_jac[idx, idx] = 1 # This is for site fraction balance constraints var_idx = 0 #len(indep_vals) for idx in range(len(dbf.phases[name].sublattices)): active_in_subl = set( dbf.phases[name].constituents[idx]).intersection(comps) constraint_jac[idx, var_idx:var_idx + len(active_in_subl)] = 1 var_idx += len(active_in_subl) newton_iteration = 0 while newton_iteration < MAX_NEWTON_ITERATIONS: flattened_points = points.reshape( points.shape[:len(indep_vals)] + (-1, points.shape[-1])) grad_args = itertools.chain( [i[..., None] for i in statevar_grid], [ flattened_points[..., i] for i in range(flattened_points.shape[-1]) ]) grad = np.array(phase_records[name].grad(*grad_args), dtype=np.float) # Remove derivatives wrt T,P grad = grad[..., len(indep_vars):] grad.shape = points.shape grad[np.isnan(grad).any( axis=-1 )] = 0 # This is necessary for gradients on the edge of space hess_args = itertools.chain( [i[..., None] for i in statevar_grid], [ flattened_points[..., i] for i in range(flattened_points.shape[-1]) ]) hess = np.array(phase_records[name].hess(*hess_args), dtype=np.float) # Remove derivatives wrt T,P hess = hess[..., len(indep_vars):, len(indep_vars):] hess.shape = points.shape + (hess.shape[-1], ) hess[np.isnan(hess).any(axis=(-2, -1))] = np.eye(hess.shape[-1]) plane_args = itertools.chain([ properties.MU.values[..., i][..., None] for i in range(properties.MU.shape[-1]) ], [points[..., i] for i in range(points.shape[-1])]) cast_grad = np.array(plane_grad(*plane_args), dtype=np.float) # Remove derivatives wrt chemical potentials cast_grad = cast_grad[..., properties.MU.shape[-1]:] grad = grad - cast_grad plane_args = itertools.chain([ properties.MU.values[..., i][..., None] for i in range(properties.MU.shape[-1]) ], [points[..., i] for i in range(points.shape[-1])]) cast_hess = np.array(plane_hess(*plane_args), dtype=np.float) # Remove derivatives wrt chemical potentials cast_hess = cast_hess[..., properties.MU.shape[-1]:, properties.MU.shape[-1]:] cast_hess = -cast_hess + hess hess = cast_hess.astype(np.float, copy=False) try: e_matrix = np.linalg.inv(hess) except np.linalg.LinAlgError: print(hess) raise current = calculate( dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])), **grid_opts) current_plane = np.multiply( current.X.values.reshape(points.shape[:-1] + (len(components), )), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) current_df = current.GM.values.reshape( points.shape[:-1]) - current_plane #print('Inv hess check: ', np.isnan(e_matrix).any()) #print('grad check: ', np.isnan(grad).any()) dy_unconstrained = -np.einsum('...ij,...j->...i', e_matrix, grad) #print('dy_unconstrained check: ', np.isnan(dy_unconstrained).any()) proj_matrix = np.dot(e_matrix, constraint_jac.T) inv_matrix = np.rollaxis(np.dot(constraint_jac, proj_matrix), 0, -1) inv_term = np.linalg.inv(inv_matrix) #print('inv_term check: ', np.isnan(inv_term).any()) first_term = np.einsum('...ij,...jk->...ik', proj_matrix, inv_term) #print('first_term check: ', np.isnan(first_term).any()) # Normally a term for the residual here # We only choose starting points which obey the constraints, so r = 0 cons_summation = np.einsum('...i,...ji->...j', dy_unconstrained, constraint_jac) #print('cons_summation check: ', np.isnan(cons_summation).any()) cons_correction = np.einsum('...ij,...j->...i', first_term, cons_summation) #print('cons_correction check: ', np.isnan(cons_correction).any()) dy_constrained = dy_unconstrained - cons_correction #print('dy_constrained check: ', np.isnan(dy_constrained).any()) # TODO: Support for adaptive changing independent variable steps new_direction = dy_constrained #print('new_direction', new_direction) #print('points', points) # Backtracking line search if np.isnan(new_direction).any(): print('new_direction', new_direction) #print('Convergence angle:', -(grad*new_direction).sum(axis=-1) / (np.linalg.norm(grad, axis=-1) * np.linalg.norm(new_direction, axis=-1))) new_points = points + INITIAL_STEP_SIZE * new_direction alpha = np.full(new_points.shape[:-1], INITIAL_STEP_SIZE, dtype=np.float) alpha[np.all(np.linalg.norm(new_direction, axis=-1) < MIN_DIRECTION_NORM, axis=-1)] = 0 negative_points = np.any(new_points < 0., axis=-1) while np.any(negative_points): alpha[negative_points] *= 0.5 new_points = points + alpha[..., np.newaxis] * new_direction negative_points = np.any(new_points < 0., axis=-1) # Backtracking line search # alpha now contains maximum possible values that keep us inside the space # but we don't just want to take the biggest step; we want the biggest step which reduces energy new_points = new_points.reshape( new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) candidates = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=new_points, **grid_opts) candidate_plane = np.multiply( candidates.X.values.reshape(points.shape[:-1] + (len(components), )), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) energy_diff = (candidates.GM.values.reshape( new_direction.shape[:-1]) - candidate_plane) - current_df new_points.shape = new_direction.shape bad_steps = energy_diff > alpha * 1e-4 * (new_direction * grad).sum(axis=-1) backtracking_iterations = 0 while np.any(bad_steps): alpha[bad_steps] *= 0.5 new_points = points + alpha[..., np.newaxis] * new_direction #print('new_points', new_points) #print('bad_steps', bad_steps) new_points = new_points.reshape( new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) candidates = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=new_points, **grid_opts) candidate_plane = np.multiply( candidates.X.values.reshape(points.shape[:-1] + (len(components), )), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) energy_diff = (candidates.GM.values.reshape( new_direction.shape[:-1]) - candidate_plane) - current_df #print('energy_diff', energy_diff) new_points.shape = new_direction.shape bad_steps = energy_diff > alpha * 1e-4 * ( new_direction * grad).sum(axis=-1) backtracking_iterations += 1 if backtracking_iterations > MAX_BACKTRACKING: break biggest_step = np.max( np.linalg.norm(new_points - points, axis=-1)) if biggest_step < 1e-2: if verbose: print('N-R convergence on mini-iteration', newton_iteration, '[{}]'.format(name)) points = new_points break if verbose: #print('Biggest step:', biggest_step) #print('points', points) #print('grad of points', grad) #print('new_direction', new_direction) #print('alpha', alpha) #print('new_points', new_points) pass points = new_points newton_iteration += 1 new_points = points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])) new_points = np.concatenate( (current_site_fractions[..., :dof], new_points), axis=-2) points_dict[name] = new_points if verbose: print('Rebuilding grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, points=points_dict, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties.attrs['iterations'] += 1 # One last call to ensure 'properties' and 'grid' are consistent with one another lower_convex_hull(grid, properties) ravelled_X_view = grid['X'].values.view().reshape( -1, grid['X'].values.shape[-1]) ravelled_Y_view = grid['Y'].values.view().reshape( -1, grid['Y'].values.shape[-1]) ravelled_Phase_view = grid['Phase'].values.view().reshape(-1) # Copy final point values from the grid and drop the index array # For some reason direct construction doesn't work. We have to create empty and then assign. properties['X'] = xray.DataArray( np.empty_like(ravelled_X_view[properties['points'].values]), dims=properties['points'].dims + ('component', )) properties['X'].values[...] = ravelled_X_view[properties['points'].values] properties['Y'] = xray.DataArray( np.empty_like(ravelled_Y_view[properties['points'].values]), dims=properties['points'].dims + ('internal_dof', )) properties['Y'].values[...] = ravelled_Y_view[properties['points'].values] # TODO: What about invariant reactions? We should perform a final driving force calculation here. # We can handle that in the same post-processing step where we identify single-phase regions. properties['Phase'] = xray.DataArray(np.empty_like( ravelled_Phase_view[properties['points'].values]), dims=properties['points'].dims) properties['Phase'].values[...] = ravelled_Phase_view[ properties['points'].values] del properties['points'] return properties
def time_calculate_magnetic(self): calculate(self.db, ['AL', 'FE', 'VA'], 'B2_BCC', T=(300, 2000, 10))
def tieline_error( dbf, comps, current_phase, cond_dict, region_chemical_potentials, phase_flag, phase_models, parameters, debug_mode=False, massfuncs=None, massgradfuncs=None, callables=None, grad_callables=None, hess_callables=None, ): """ Parameters ---------- dbf : pycalphad.Database Database to consider comps : list List of active component names current_phase : list List of phases to consider current_statevars : dict Dictionary of state variables, e.g. v.P and v.T, no compositions. comp_dicts : list List of tuples of composition dictionaries and phase flags. Composition dictionaries are pycalphad variable dicts and the flag is a string e.g. ({v.X('CU'): 0.5}, 'disordered') phase_models : dict Phase models to pass to pycalphad calculations parameters : dict Dictionary of symbols that will be overridden in pycalphad.equilibrium massfuncs : dict Callables of mass derivatives to pass to pycalphad massgradfuncs : dict Gradient callables of mass derivatives to pass to pycalphad callables : dict Callables to pass to pycalphad grad_callables : dict Gradient callables to pass to pycalphad hess_callables : dict Hessian callables to pass to pycalphad cond_dict : region_chemical_potentials : numpy.ndarray Array of chemical potentials for target equilibrium hyperplane. phase_flag : str String of phase flag, e.g. 'disordered'. phase_models : dict Phase models to pass to pycalphad calculations parameters : dict Dictionary of symbols that will be overridden in pycalphad.equilibrium debug_mode : bool If True, will write out scripts when pycalphad fails to find a stable equilibrium. These scripts can be used to debug pycalphad. massfuncs : dict Callables of mass derivatives to pass to pycalphad massgradfuncs : dict Gradient callables of mass derivatives to pass to pycalphad callables : dict Callables to pass to pycalphad grad_callables : dict Gradient callables to pass to pycalphad hess_callables : dict Hessian callables to pass to pycalphad Returns ------- float Single value for the total error between the current hyperplane and target hyperplane. """ if np.any(np.isnan(list(cond_dict.values()))): # We don't actually know the phase composition here, so we estimate it single_eqdata = calculate( dbf, comps, [current_phase], T=cond_dict[v.T], P=cond_dict[v.P], model=phase_models, parameters=parameters, pdens=100, massfuncs=massfuncs, callables=callables, ) driving_force = np.multiply(region_chemical_potentials, single_eqdata['X'].values).sum( axis=-1) - single_eqdata['GM'].values error = float(driving_force.max()) elif phase_flag == 'disordered': # Construct disordered sublattice configuration from composition dict # Compute energy # Compute residual driving force # TODO: Check that it actually makes sense to declare this phase 'disordered' num_dof = sum([ len(set(c).intersection(comps)) for c in dbf.phases[current_phase].constituents ]) desired_sitefracs = np.ones(num_dof, dtype=np.float) dof_idx = 0 for c in dbf.phases[current_phase].constituents: dof = sorted(set(c).intersection(comps)) if (len(dof) == 1) and (dof[0] == 'VA'): return 0 # If it's disordered config of BCC_B2 with VA, disordered config is tiny vacancy count sitefracs_to_add = np.array([cond_dict.get(v.X(d)) for d in dof], dtype=np.float) # Fix composition of dependent component sitefracs_to_add[np.isnan( sitefracs_to_add)] = 1 - np.nansum(sitefracs_to_add) desired_sitefracs[dof_idx:dof_idx + len(dof)] = sitefracs_to_add dof_idx += len(dof) single_eqdata = calculate( dbf, comps, [current_phase], T=cond_dict[v.T], P=cond_dict[v.P], points=desired_sitefracs, model=phase_models, parameters=parameters, massfuncs=massfuncs, callables=callables, ) driving_force = np.multiply(region_chemical_potentials, single_eqdata['X'].values).sum( axis=-1) - single_eqdata['GM'].values error = float(np.squeeze(driving_force)) else: # Extract energies from single-phase calculations single_eqdata = equilibrium( dbf, comps, [current_phase], cond_dict, verbose=False, model=phase_models, scheduler=dask.local.get_sync, parameters=parameters, massfuncs=massfuncs, massgradfuncs=massgradfuncs, callables=callables, grad_callables=grad_callables, hess_callables=hess_callables, ) if np.all(np.isnan(single_eqdata['NP'].values)): error_time = time.time() template_error = """ from pycalphad import Database, equilibrium from pycalphad.variables import T, P, X import dask dbf_string = \"\"\" {0} \"\"\" dbf = Database(dbf_string) comps = {1} phases = {2} cond_dict = {3} parameters = {4} equilibrium(dbf, comps, phases, cond_dict, scheduler=dask.local.get_sync, parameters=parameters) """ template_error = textwrap.dedent(template_error) if debug_mode: logging.warning('Dumping', 'error-' + str(error_time) + '.py') with open('error-' + str(error_time) + '.py', 'w') as f: f.write( template_error.format( dbf.to_string(fmt='tdb'), comps, [current_phase], cond_dict, {key: float(x) for key, x in parameters.items()})) # Sometimes we can get a miscibility gap in our "single-phase" calculation # Choose the weighted mixture of site fractions logging.debug( 'Calculation failure: all NaN phases with conditions: {}'. format(cond_dict)) return 0 select_energy = float(single_eqdata['GM'].values) region_comps = [] for comp in [c for c in sorted(comps) if c != 'VA']: region_comps.append(cond_dict.get(v.X(comp), np.nan)) region_comps[region_comps.index(np.nan)] = 1 - np.nansum(region_comps) error = np.multiply(region_chemical_potentials, region_comps).sum() - select_energy error = float(error) return error
def calculate_thermochemical_error(dbf, comps, phases, datasets, parameters=None, phase_models=None, callables=None): """ Calculate the weighted single phase error in the Database Parameters ---------- dbf : pycalphad.Database Database to consider comps : list List of active component names phases : list List of phases to consider datasets : espei.utils.PickleableTinyDB Datasets that contain single phase data parameters : dict Dictionary of symbols that will be overridden in pycalphad.calculate phase_models : dict Phase models to pass to pycalphad calculations. Ideal mixing contributions must be removed. callables : dict Dictionary of {output_property: callables_dict} where callables_dict is a dictionary of {phase_name: callables} to pass to pycalphad. These must have ideal mixing portions removed. Returns ------- float A single float of the residual sum of square errors Notes ----- There are different single phase values, HM_MIX, SM_FORM, CP_FORM, etc. Each of these have different units and the error cannot be compared directly. To normalize all of the errors, a normalization factor must be used. Equation 2.59 and 2.60 in Lukas, Fries, and Sundman "Computational Thermodynamics" shows how this can be considered. Each type of error will be weighted by the reciprocal of the estimated uncertainty in the measured value and conditions. The weighting factor is calculated by $ p_i = (\Delta L_i)^{-1} $ where $ \Delta L_i $ is the uncertainty in the measurement. We will neglect the uncertainty for quantities such as temperature, assuming they are small. """ if parameters is None: parameters = {} if phase_models is None: # create phase models with ideal mixing removed phase_models = {} for phase_name in phases: phase_models[phase_name] = Model(dbf, comps, phase_name) phase_models[phase_name].models['idmix'] = 0 # property weights factors as fractions of the parameters # for now they are all set to 5% property_prefix_weight_factor = { 'HM': 0.05, 'SM': 0.05, 'CPM': 0.05, } property_suffixes = ('_FORM', '_MIX') # the kinds of properties, e.g. 'HM'+suffix =>, 'HM_FORM', 'HM_MIX' # we could also include the bare property ('' => 'HM'), but these are rarely used in ESPEI properties = [ ''.join(prop) for prop in itertools.product( property_prefix_weight_factor.keys(), property_suffixes) ] whitelist_properties = ['HM', 'SM', 'CPM', 'HM_MIX', 'SM_MIX', 'CPM_MIX'] # if callables is None, construct empty callables dicts, which will be JIT compiled by pycalphad later callables = callables if callables is not None else { prop: None for prop in whitelist_properties } sum_square_error = 0 for phase_name in phases: for prop in properties: desired_data = get_prop_data(comps, phase_name, prop, datasets) if len(desired_data) == 0: # logging.debug('Skipping {} in phase {} because no data was found.'.format(prop, phase_name)) continue calculate_dict = get_prop_samples(dbf, comps, phase_name, desired_data) if prop.endswith('_FORM'): calculate_dict['output'] = ''.join(prop.split('_')[:-1]) params = parameters.copy() params.update( {'GHSER' + (c.upper() * 2)[:2]: 0 for c in comps}) else: calculate_dict['output'] = prop params = parameters sample_values = calculate_dict.pop('values') results = calculate( dbf, comps, phase_name, broadcast=False, parameters=params, model=phase_models, callables=callables[calculate_dict['output']], **calculate_dict)[calculate_dict['output']].values weight = (property_prefix_weight_factor[prop.split('_')[0]] * np.abs(np.mean(sample_values)))**(-1.0) error = np.sum((results - sample_values)**2) * weight # logging.debug('Weighted sum of square error for property {} of phase {}: {}'.format(prop, phase_name, error)) sum_square_error += error return -sum_square_error
def map_binary( dbf, comps, phases, conds, eq_kwargs=None, calc_kwargs=None, boundary_sets=None, verbose=False, summary=False, ): """ Map a binary T-X phase diagram Parameters ---------- dbf : Database comps : list of str phases : list of str List of phases to consider in mapping conds : dict Dictionary of conditions eq_kwargs : dict Dictionary of keyword arguments to pass to equilibrium verbose : bool Print verbose output for mapping boundary_sets : ZPFBoundarySets Existing ZPFBoundarySets Returns ------- ZPFBoundarySets Notes ----- Assumes conditions in T and X. Simple algorithm to map a binary phase diagram in T-X. More or less follows the algorithm described in Figure 2 by Snider et al. [1] with the small algorithmic improvement of constructing a convex hull to find the next potential two phase region. For each temperature, proceed along increasing composition, skipping two over two phase regions, once calculated. [1] J. Snider, I. Griva, X. Sun, M. Emelianenko, Set based framework for Gibbs energy minimization, Calphad. 48 (2015) 18-26. doi: 10.1016/j.calphad.2014.09.005 """ eq_kwargs = eq_kwargs or {} calc_kwargs = calc_kwargs or {} # implicitly add v.N to conditions if v.N not in conds: conds[v.N] = [1.0] if 'pdens' not in calc_kwargs: calc_kwargs['pdens'] = 2000 species = unpack_components(dbf, comps) phases = filter_phases(dbf, species, phases) parameters = eq_kwargs.get('parameters', {}) models = eq_kwargs.get('model') statevars = get_state_variables(models=models, conds=conds) if models is None: models = instantiate_models(dbf, comps, phases, model=eq_kwargs.get('model'), parameters=parameters, symbols_only=True) prxs = build_phase_records(dbf, species, phases, conds, models, output='GM', parameters=parameters, build_gradients=True, build_hessians=True) indep_comp = [ key for key, value in conds.items() if isinstance(key, v.MoleFraction) and len(np.atleast_1d(value)) > 1 ] indep_pot = [ key for key, value in conds.items() if (type(key) is v.StateVariable) and len(np.atleast_1d(value)) > 1 ] if (len(indep_comp) != 1) or (len(indep_pot) != 1): raise ValueError( 'Binary map requires exactly one composition and one potential coordinate' ) if indep_pot[0] != v.T: raise ValueError( 'Binary map requires that a temperature grid must be defined') # binary assumption, only one composition specified. comp_cond = [k for k in conds.keys() if isinstance(k, v.X)][0] indep_comp = comp_cond.name[2:] indep_comp_idx = sorted(get_pure_elements(dbf, comps)).index(indep_comp) composition_grid = unpack_condition(conds[comp_cond]) dX = composition_grid[1] - composition_grid[0] Xmax = composition_grid.max() temperature_grid = unpack_condition(conds[v.T]) dT = temperature_grid[1] - temperature_grid[0] boundary_sets = boundary_sets or ZPFBoundarySets(comps, comp_cond) equilibria_calculated = 0 equilibrium_time = 0 convex_hulls_calculated = 0 convex_hull_time = 0 curr_conds = {key: unpack_condition(val) for key, val in conds.items()} str_conds = sorted([str(k) for k in curr_conds.keys()]) grid_conds = _adjust_conditions(curr_conds) for T_idx in range(temperature_grid.size): T = temperature_grid[T_idx] iter_equilibria = 0 if verbose: print("=== T = {} ===".format(float(T))) curr_conds[v.T] = [float(T)] eq_conds = deepcopy(curr_conds) Xmax_visited = 0.0 hull_time = time.time() grid = calculate(dbf, comps, phases, fake_points=True, output='GM', T=T, P=grid_conds[v.P], N=1, model=models, parameters=parameters, to_xarray=False, **calc_kwargs) hull = starting_point(eq_conds, statevars, prxs, grid) convex_hull_time += time.time() - hull_time convex_hulls_calculated += 1 while Xmax_visited < Xmax: hull_compsets = find_two_phase_region_compsets( hull, T, indep_comp, indep_comp_idx, minimum_composition=Xmax_visited, misc_gap_tol=2 * dX) if hull_compsets is None: if verbose: print( "== Convex hull: max visited = {} - no multiphase phase compsets found ==" .format(Xmax_visited, hull_compsets)) break Xeq = hull_compsets.mean_composition eq_conds[comp_cond] = [float(Xeq)] eq_time = time.time() start_point = starting_point(eq_conds, statevars, prxs, grid) eq_ds = _solve_eq_at_conditions(species, start_point, prxs, grid, str_conds, statevars, False) equilibrium_time += time.time() - eq_time equilibria_calculated += 1 iter_equilibria += 1 # composition sets in the plane of the calculation: # even for isopleths, this should always be two. compsets = get_compsets(eq_ds, indep_comp, indep_comp_idx) if verbose: print( "== Convex hull: max visited = {:0.4f} - hull compsets: {} equilibrium compsets: {} ==" .format(Xmax_visited, hull_compsets, compsets)) if compsets is None: # equilibrium calculation, didn't find a valid multiphase composition set # we need to find the next feasible one from the convex hull. Xmax_visited += dX continue else: boundary_sets.add_compsets(compsets, Xtol=0.10, Ttol=2 * dT) if compsets.max_composition > Xmax_visited: Xmax_visited = compsets.max_composition # this seems kind of sloppy, but captures the effect that we want to # keep doing equilibrium calculations, if possible. while Xmax_visited < Xmax and compsets is not None: eq_conds[comp_cond] = [float(Xmax_visited + dX)] eq_time = time.time() # TODO: starting point could be improved by basing it off the previous calculation start_point = starting_point(eq_conds, statevars, prxs, grid) eq_ds = _solve_eq_at_conditions(species, start_point, prxs, grid, str_conds, statevars, False) equilibrium_time += time.time() - eq_time equilibria_calculated += 1 compsets = get_compsets(eq_ds, indep_comp, indep_comp_idx) if compsets is not None: Xmax_visited = compsets.max_composition boundary_sets.add_compsets(compsets, Xtol=0.10, Ttol=2 * dT) else: Xmax_visited += dX if verbose: print("Equilibrium: at X = {:0.4f}, found compsets {}". format(Xmax_visited, compsets)) if verbose: print(iter_equilibria, 'equilibria calculated in this iteration.') if verbose or summary: print("{} Convex hulls calculated ({:0.1f}s)".format( convex_hulls_calculated, convex_hull_time)) print("{} Equilbria calculated ({:0.1f}s)".format( equilibria_calculated, equilibrium_time)) print("{:0.0f}% of brute force calculations skipped".format( 100 * (1 - equilibria_calculated / (composition_grid.size * temperature_grid.size)))) return boundary_sets
def _eqcalculate(dbf, comps, phases, conditions, output, data=None, per_phase=False, callables=None, parameters=None, **kwargs): """ WARNING: API/calling convention not finalized. Compute the *equilibrium value* of a property. This function differs from `calculate` in that it computes thermodynamic equilibrium instead of randomly sampling the internal degrees of freedom of a phase. Because of that, it's slower than `calculate`. This plugs in the equilibrium phase and site fractions to compute a thermodynamic property defined in a Model. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : list Names of components to consider in the calculation. phases : list or dict Names of phases to consider in the calculation. conditions : dict or (list of dict) StateVariables and their corresponding value. output : str Equilibrium model property (e.g., CPM, HM, etc.) to compute. This must be defined as an attribute in the Model class of each phase. data : Dataset, optional Previous result of call to `equilibrium`. Should contain the equilibrium configurations at the conditions of interest. If the databases are not the same as in the original calculation, the results may be meaningless. If None, `equilibrium` will be called. Specifying this keyword argument can save the user some time if several properties need to be calculated in succession. per_phase : bool, optional If True, compute and return the property for each phase present. If False, return the total system value, weighted by the phase fractions. parameters : dict, optional Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database. callables : dict Callable functions to compute 'output' for each phase. kwargs Passed to `calculate`. Returns ------- Dataset of property as a function of equilibrium conditions """ if data is None: data = equilibrium(dbf, comps, phases, conditions) active_phases = unpack_phases(phases) or sorted(dbf.phases.keys()) conds = _adjust_conditions(conditions) indep_vars = ['N', 'P', 'T'] # TODO: Rewrite this to use the coord dict from 'data' str_conds = OrderedDict((str(key), value) for key, value in conds.items()) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) coord_dict = str_conds.copy() components = [x for x in sorted(comps)] desired_active_pure_elements = [list(x.constituents.keys()) for x in components] desired_active_pure_elements = [el.upper() for constituents in desired_active_pure_elements for el in constituents] pure_elements = sorted(set([x for x in desired_active_pure_elements if x != 'VA'])) coord_dict['vertex'] = np.arange(len(pure_elements) + 1) # +1 is to accommodate the degenerate degree of freedom at the invariant reactions grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape prop_shape = grid_shape prop_dims = list(str_conds.keys()) + ['vertex'] result = Dataset({output: (prop_dims, np.full(prop_shape, np.nan))}, coords=coord_dict) # For each phase select all conditions where that phase exists # Perform the appropriate calculation and then write the result back for phase in active_phases: dof = sum([len(x) for x in dbf.phases[phase].constituents]) current_phase_indices = (data.Phase.values == phase) if ~np.any(current_phase_indices): continue points = data.Y.values[np.nonzero(current_phase_indices)][..., :dof] statevar_indices = np.nonzero(current_phase_indices)[:len(indep_vals)] statevars = {key: np.take(np.asarray(vals), idx) for key, vals, idx in zip(indep_vars, indep_vals, statevar_indices)} statevars.update(kwargs) if statevars.get('mode', None) is None: statevars['mode'] = 'numpy' calcres = calculate(dbf, comps, [phase], output=output, points=points, broadcast=False, callables=callables, parameters=parameters, **statevars) result[output].values[np.nonzero(current_phase_indices)] = calcres[output].values if not per_phase: result[output] = (result[output] * data['NP']).sum(dim='vertex', skipna=True) else: result['Phase'] = data['Phase'].copy() result['NP'] = data['NP'].copy() return result
def _eqcalculate(dbf, comps, phases, conditions, output, data=None, per_phase=False, **kwargs): """ WARNING: API/calling convention not finalized. Compute the *equilibrium value* of a property. This function differs from `calculate` in that it computes thermodynamic equilibrium instead of randomly sampling the internal degrees of freedom of a phase. Because of that, it's slower than `calculate`. This plugs in the equilibrium phase and site fractions to compute a thermodynamic property defined in a Model. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : list Names of components to consider in the calculation. phases : list or dict Names of phases to consider in the calculation. conditions : dict or (list of dict) StateVariables and their corresponding value. output : str Equilibrium model property (e.g., CPM, HM, etc.) to compute. This must be defined as an attribute in the Model class of each phase. data : Dataset, optional Previous result of call to `equilibrium`. Should contain the equilibrium configurations at the conditions of interest. If the databases are not the same as in the original calculation, the results may be meaningless. If None, `equilibrium` will be called. Specifying this keyword argument can save the user some time if several properties need to be calculated in succession. per_phase : bool, optional If True, compute and return the property for each phase present. If False, return the total system value, weighted by the phase fractions. kwargs Passed to `calculate`. Returns ------- Dataset of property as a function of equilibrium conditions """ if data is None: data = equilibrium(dbf, comps, phases, conditions) active_phases = unpack_phases(phases) or sorted(dbf.phases.keys()) conds = _adjust_conditions(conditions) indep_vars = ['P', 'T'] # TODO: Rewrite this to use the coord dict from 'data' str_conds = OrderedDict((str(key), value) for key, value in conds.items()) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) coord_dict = str_conds.copy() components = [x for x in sorted(comps) if not x.startswith('VA')] coord_dict['vertex'] = np.arange(len(components)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape prop_shape = grid_shape prop_dims = list(str_conds.keys()) + ['vertex'] result = Dataset({output: (prop_dims, np.full(prop_shape, np.nan))}, coords=coord_dict) # For each phase select all conditions where that phase exists # Perform the appropriate calculation and then write the result back for phase in active_phases: dof = sum([len(x) for x in dbf.phases[phase].constituents]) current_phase_indices = (data.Phase.values == phase) if ~np.any(current_phase_indices): continue points = data.Y.values[np.nonzero(current_phase_indices)][..., :dof] statevar_indices = np.nonzero(current_phase_indices)[:len(indep_vals)] statevars = { key: np.take(np.asarray(vals), idx) for key, vals, idx in zip(indep_vars, indep_vals, statevar_indices) } statevars.update(kwargs) if statevars.get('mode', None) is None: statevars['mode'] = 'numpy' calcres = calculate(dbf, comps, [phase], output=output, points=points, broadcast=False, **statevars) result[output].values[np.nonzero( current_phase_indices)] = calcres[output].values if not per_phase: result[output] = (result[output] * data['NP']).sum(dim='vertex', skipna=True) else: result['Phase'] = data['Phase'].copy() result['NP'] = data['NP'].copy() return result
def test_calculate_raises_with_no_active_phases_passed(): """Passing inactive phases to calculate() raises a ConditionError.""" # Phase cannot be built without FE calculate(ALFE_DBF, ['AL', 'VA'], ['AL13FE4'], T=1200, P=101325)
def test_calculate_raises_with_no_active_phases_passed(): """Passing inactive phases to calculate() raises a ConditionError.""" # Phase cannot be built without FE with pytest.raises(ConditionError): calculate(ALFE_DBF, ['AL', 'VA'], ['AL13FE4'], T=1200, P=101325)
def test_single_model_instance_raises(): "Calculate raises when a single Model instance is passed with multiple phases." comps = ['AL', 'CR', 'NI'] phase_name = 'L12_FCC' mod = Model(DBF, comps, 'L12_FCC') # Model instance does not match the phase calculate(DBF, comps, ['LIQUID', 'L12_FCC'], T=1400.0, output='_fail_', model=mod)
def equilibrium(dbf, comps, phases, conditions, **kwargs): """ Calculate the equilibrium state of a system containing the specified components and phases, under the specified conditions. Model parameters are taken from 'dbf'. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : list Names of components to consider in the calculation. phases : list or dict Names of phases to consider in the calculation. conditions : dict or (list of dict) StateVariables and their corresponding value. verbose : bool, optional (Default: True) Show progress of calculations. grid_opts : dict, optional Keyword arguments to pass to the initial grid routine. Returns ------- Structured equilibrium calculation. Examples -------- None yet. """ active_phases = unpack_phases(phases) or sorted(dbf.phases.keys()) comps = sorted(comps) indep_vars = ['T', 'P'] grid_opts = kwargs.pop('grid_opts', dict()) verbose = kwargs.pop('verbose', True) phase_records = dict() callable_dict = kwargs.pop('callables', dict()) grad_callable_dict = kwargs.pop('grad_callables', dict()) points_dict = dict() maximum_internal_dof = 0 # Construct models for each phase; prioritize user models models = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) if verbose: print('Components:', ' '.join(comps)) print('Phases:', end=' ') for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name) variables = sorted(mod.energy.atoms(v.StateVariable).union({key for key in conditions.keys() if key in [v.T, v.P]}), key=str) site_fracs = sorted(mod.energy.atoms(v.SiteFraction), key=str) maximum_internal_dof = max(maximum_internal_dof, len(site_fracs)) # Extra factor '1e-100...' is to work around an annoying broadcasting bug for zero gradient entries models[name].models['_broadcaster'] = 1e-100 * Mul(*variables) ** 3 out = models[name].energy if name not in callable_dict: undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) # callable_dict takes variables in a different order due to calculate() pecularities callable_dict[name] = make_callable(out, sorted((key for key in conditions.keys() if key in [v.T, v.P]), key=str) + site_fracs) if name not in grad_callable_dict: grad_func = make_callable(Matrix([out]).jacobian(variables), variables) else: grad_func = grad_callable_dict[name] # Adjust gradient by the approximate chemical potentials plane_vars = sorted(models[name].energy.atoms(v.SiteFraction), key=str) hyperplane = Add(*[v.MU(i)*mole_fraction(dbf.phases[name], comps, i) for i in comps if i != 'VA']) # Workaround an annoying bug with zero gradient entries # This forces numerically zero entries to broadcast correctly hyperplane += 1e-100 * Mul(*([v.MU(i) for i in comps if i != 'VA'] + plane_vars + [v.T, v.P])) ** 3 plane_grad = make_callable(Matrix([hyperplane]).jacobian(variables), [v.MU(i) for i in comps if i != 'VA'] + plane_vars + [v.T, v.P]) plane_hess = make_callable(hessian(hyperplane, variables), [v.MU(i) for i in comps if i != 'VA'] + plane_vars + [v.T, v.P]) phase_records[name.upper()] = PhaseRecord(variables=variables, grad=grad_func, plane_grad=plane_grad, plane_hess=plane_hess) if verbose: print(name, end=' ') if verbose: print('[done]', end='\n') conds = OrderedDict((key, unpack_condition(value)) for key, value in sorted(conditions.items(), key=str)) str_conds = OrderedDict((str(key), value) for key, value in conds.items()) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) components = [x for x in sorted(comps) if not x.startswith('VA')] # 'calculate' accepts conditions through its keyword arguments grid_opts.update({key: value for key, value in str_conds.items() if key in indep_vars}) if 'pdens' not in grid_opts: grid_opts['pdens'] = 10 coord_dict = str_conds.copy() coord_dict['vertex'] = np.arange(len(components)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape coord_dict['component'] = components if verbose: print('Computing initial grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties = xray.Dataset({'NP': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape)), 'GM': (list(str_conds.keys()), np.empty(grid_shape[:-1])), 'MU': (list(str_conds.keys()) + ['component'], np.empty(grid_shape)), 'points': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype=np.int)) }, coords=coord_dict, attrs={'iterations': 1}, ) # Store the potentials from the previous iteration current_potentials = properties.MU.copy() for iteration in range(MAX_ITERATIONS): if verbose: print('Computing convex hull [iteration {}]'.format(properties.attrs['iterations'])) # lower_convex_hull will modify properties lower_convex_hull(grid, properties) progress = np.abs(current_potentials - properties.MU).max().values if verbose: print('progress', progress) if progress < MIN_PROGRESS: if verbose: print('Convergence achieved') break current_potentials[...] = properties.MU.values if verbose: print('Refining convex hull') # Insert extra dimensions for non-T,P conditions so GM broadcasts correctly energy_broadcast_shape = grid.GM.values.shape[:len(indep_vals)] + \ (1,) * (len(str_conds) - len(indep_vals)) + (grid.GM.values.shape[-1],) driving_forces = np.einsum('...i,...i', properties.MU.values[..., np.newaxis, :], grid.X.values[np.index_exp[...] + (np.newaxis,) * (len(str_conds) - len(indep_vals)) + np.index_exp[:, :]]) - \ grid.GM.values.view().reshape(energy_broadcast_shape) for name in active_phases: dof = len(models[name].energy.atoms(v.SiteFraction)) current_phase_indices = (grid.Phase.values == name).reshape(energy_broadcast_shape[:-1] + (-1,)) # Broadcast to capture all conditions current_phase_indices = np.broadcast_arrays(current_phase_indices, np.empty(driving_forces.shape))[0] # This reshape is safe as long as phases have the same number of points at all indep. conditions current_phase_driving_forces = driving_forces[current_phase_indices].reshape( current_phase_indices.shape[:-1] + (-1,)) # Note: This works as long as all points are in the same phase order for all T, P current_site_fractions = grid.Y.values[..., current_phase_indices[(0,) * len(str_conds)], :] if np.sum(current_site_fractions[(0,) * len(indep_vals)][..., :dof]) == dof: # All site fractions are 1, aka zero internal degrees of freedom # Impossible to refine these points, so skip this phase points_dict[name] = current_site_fractions[(0,) * len(indep_vals)][..., :dof] continue # Find the N points with largest driving force for a given set of conditions # Remember that driving force has a sign, so we want the "most positive" values # N is the number of components, in this context # N points define a 'best simplex' for every set of conditions # We also need to restrict ourselves to one phase at a time trial_indices = np.argpartition(current_phase_driving_forces, -len(components), axis=-1)[..., -len(components):] trial_indices = trial_indices.ravel() statevar_indices = np.unravel_index(np.arange(np.multiply.reduce(properties.GM.values.shape + (len(components),))), properties.GM.values.shape + (len(components),))[:len(indep_vals)] points = current_site_fractions[np.index_exp[statevar_indices + (trial_indices,)]] points.shape = properties.points.shape[:-1] + (-1, maximum_internal_dof) # The Y arrays have been padded, so we should slice off the padding points = points[..., :dof] # Workaround for derivative issues at endmembers points[points == 0.] = MIN_SITE_FRACTION if len(points) == 0: if name in points_dict: del points_dict[name] # No nearly stable points: skip this phase continue num_vars = len(phase_records[name].variables) plane_grad = phase_records[name].plane_grad plane_hess = phase_records[name].plane_hess statevar_grid = np.meshgrid(*itertools.chain(indep_vals), sparse=True, indexing='xy') # TODO: A more sophisticated treatment of constraints num_constraints = len(indep_vals) + len(dbf.phases[name].sublattices) constraint_jac = np.zeros((num_constraints, num_vars)) # Independent variables are always fixed (in this limited implementation) for idx in range(len(indep_vals)): constraint_jac[idx, idx] = 1 # This is for site fraction balance constraints var_idx = len(indep_vals) for idx in range(len(dbf.phases[name].sublattices)): active_in_subl = set(dbf.phases[name].constituents[idx]).intersection(comps) constraint_jac[len(indep_vals) + idx, var_idx:var_idx + len(active_in_subl)] = 1 var_idx += len(active_in_subl) grad = phase_records[name].grad(*itertools.chain(statevar_grid, points.T)) if grad.dtype == 'object': # Workaround a bug in zero gradient entries grad_zeros = np.zeros(points.T.shape[1:], dtype=np.float) for i in np.arange(grad.shape[0]): if isinstance(grad[i], int): grad[i] = grad_zeros grad = np.array(grad.tolist(), dtype=np.float) bcasts = np.broadcast_arrays(*itertools.chain(properties.MU.values.T, points.T)) cast_grad = -plane_grad(*itertools.chain(bcasts, [0], [0])) cast_grad = cast_grad.T + grad.T grad = cast_grad grad.shape = grad.shape[:-1] # Remove extraneous dimension # This Hessian is an approximation updated using the BFGS method # See Nocedal and Wright, ch.3, p. 198 # Initialize as identity matrix hess = broadcast_to(np.eye(num_vars), grad.shape + (grad.shape[-1],)).copy() newton_iteration = 0 while newton_iteration < MAX_NEWTON_ITERATIONS: e_matrix = np.linalg.inv(hess) dy_unconstrained = -np.einsum('...ij,...j->...i', e_matrix, grad) proj_matrix = np.dot(e_matrix, constraint_jac.T) inv_matrix = np.rollaxis(np.dot(constraint_jac, proj_matrix), 0, -1) inv_term = np.linalg.inv(inv_matrix) first_term = np.einsum('...ij,...jk->...ik', proj_matrix, inv_term) # Normally a term for the residual here # We only choose starting points which obey the constraints, so r = 0 cons_summation = np.einsum('...i,...ji->...j', dy_unconstrained, constraint_jac) cons_correction = np.einsum('...ij,...j->...i', first_term, cons_summation) dy_constrained = dy_unconstrained - cons_correction # TODO: Support for adaptive changing independent variable steps new_direction = dy_constrained[..., len(indep_vals):] # Backtracking line search new_points = points + INITIAL_STEP_SIZE * new_direction alpha = np.full(new_points.shape[:-1], INITIAL_STEP_SIZE, dtype=np.float) negative_points = np.any(new_points < 0., axis=-1) while np.any(negative_points): alpha[negative_points] *= 0.1 new_points = points + alpha[..., np.newaxis] * new_direction negative_points = np.any(new_points < 0., axis=-1) # If we made "near" zero progress on any points, don't update the Hessian until # we've rebuilt the convex hull # Nocedal and Wright recommend against skipping Hessian updates # They recommend using a damped update approach, pp. 538-539 of their book # TODO: Check the projected gradient norm, not the step length if np.any(np.max(np.abs(alpha[..., np.newaxis] * new_direction), axis=-1) < MIN_STEP_LENGTH): break # Workaround for derivative issues at endmembers new_points[new_points == 0.] = 1e-16 # BFGS update to Hessian new_grad = phase_records[name].grad(*itertools.chain(statevar_grid, new_points.T)) if new_grad.dtype == 'object': # Workaround a bug in zero gradient entries grad_zeros = np.zeros(new_points.T.shape[1:], dtype=np.float) for i in np.arange(new_grad.shape[0]): if isinstance(new_grad[i], int): new_grad[i] = grad_zeros new_grad = np.array(new_grad.tolist(), dtype=np.float) bcasts = np.broadcast_arrays(*itertools.chain(properties.MU.values.T, new_points.T)) cast_grad = -plane_grad(*itertools.chain(bcasts, [0], [0])) cast_grad = cast_grad.T + new_grad.T new_grad = cast_grad new_grad.shape = new_grad.shape[:-1] # Remove extraneous dimension # Notation used here consistent with Nocedal and Wright s_k = np.empty(points.shape[:-1] + (points.shape[-1] + len(indep_vals),)) # Zero out independent variable changes for now s_k[..., :len(indep_vals)] = 0 s_k[..., len(indep_vals):] = new_points - points y_k = new_grad - grad s_s_term = np.einsum('...j,...k->...jk', s_k, s_k) s_b_s_term = np.einsum('...i,...ij,...j', s_k, hess, s_k) y_y_y_s_term = np.einsum('...j,...k->...jk', y_k, y_k) / \ np.einsum('...i,...i', y_k, s_k)[..., np.newaxis, np.newaxis] update = np.einsum('...ij,...jk,...kl->...il', hess, s_s_term, hess) / \ s_b_s_term[..., np.newaxis, np.newaxis] + y_y_y_s_term hess = hess - update cast_hess = -plane_hess(*itertools.chain(bcasts, [0], [0])).T + hess hess = -cast_hess #TODO: Why does this fix things? # TODO: Verify that the chosen step lengths reduce the energy points = new_points grad = new_grad newton_iteration += 1 new_points = new_points.reshape(new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) new_points = np.concatenate((current_site_fractions[..., :dof], new_points), axis=-2) points_dict[name] = new_points if verbose: print('Rebuilding grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, points=points_dict, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties.attrs['iterations'] += 1 # One last call to ensure 'properties' and 'grid' are consistent with one another lower_convex_hull(grid, properties) ravelled_X_view = grid['X'].values.view().reshape(-1, grid['X'].values.shape[-1]) ravelled_Y_view = grid['Y'].values.view().reshape(-1, grid['Y'].values.shape[-1]) ravelled_Phase_view = grid['Phase'].values.view().reshape(-1) # Copy final point values from the grid and drop the index array # For some reason direct construction doesn't work. We have to create empty and then assign. properties['X'] = xray.DataArray(np.empty_like(ravelled_X_view[properties['points'].values]), dims=properties['points'].dims + ('component',)) properties['X'].values[...] = ravelled_X_view[properties['points'].values] properties['Y'] = xray.DataArray(np.empty_like(ravelled_Y_view[properties['points'].values]), dims=properties['points'].dims + ('internal_dof',)) properties['Y'].values[...] = ravelled_Y_view[properties['points'].values] # TODO: What about invariant reactions? We should perform a final driving force calculation here. # We can handle that in the same post-processing step where we identify single-phase regions. properties['Phase'] = xray.DataArray(np.empty_like(ravelled_Phase_view[properties['points'].values]), dims=properties['points'].dims) properties['Phase'].values[...] = ravelled_Phase_view[properties['points'].values] del properties['points'] return properties