Skip to content

Commit

Permalink
ENH: Generalize phase composition support (#35)
Browse files Browse the repository at this point in the history
Until now we have only tracked liquid phase compositions in `x_liquid`. Now we are tracking phase compositions of all phases. Note that we don't yet support phase multiplicities, so miscibility gaps _will_ give potentially meaningless results. This change also introduces tracking of the phase composition of the dependent component as well, for convenience.

We are keeping `x_liquid` around for now for backwards compatibility, but eventually it should probably be removed and superseded by `phase_compositions` (since the liquid phase is in the `phase_compositions` dictionary).
  • Loading branch information
bocklund authored Jul 22, 2024
1 parent ff82d79 commit 90a0e20
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 21 deletions.
51 changes: 39 additions & 12 deletions scheil/simulate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import sys
from typing import Mapping, List
import numpy as np
from pycalphad import equilibrium, variables as v
from pycalphad.core.light_dataset import LightDataset
from pycalphad.codegen.phase_record_factory import PhaseRecordFactory
from pycalphad.core.calculate import _sample_phase_constitution
from pycalphad.core.utils import instantiate_models, unpack_components, filter_phases, point_sample
Expand Down Expand Up @@ -55,6 +57,34 @@ def _update_points(eq, points_dict, dof_dict, local_pdens=0, verbose=False):
points_dict[ph] = np.concatenate([pts, eq_pts], axis=0)


def _update_phase_compositions(phase_compositions: Mapping[str, Mapping[str, List[float]]], eq_res):
"""
Parameters
----------
phase_compositions : Mapping[PhaseName, Mapping[ComponentName, List[float]]]
eq_res : xarray.Dataset
From PyCalphad equilibrium
"""
if isinstance(eq_res, LightDataset):
eq_res = eq_res.get_dataset() # slight performance hit, but hopefully we shouldn't need this once we fully adopt Workspace
phase_compositions_accounted_for = {''}
for vertex in range(eq_res.vertex.size):
phase_name = str(eq_res.Phase.squeeze().values[vertex])
if phase_name in phase_compositions_accounted_for:
# Skip phases we have already counted
# this will _not_ count phases with a miscibility gap! we need to include pycalphad multiplicity support
continue
for comp in phase_compositions[phase_name].keys():
x = float(eq_res["X"].isel(vertex=vertex).squeeze().sel(component=comp).values)
phase_compositions[phase_name][comp].append(x)
phase_compositions_accounted_for.add(phase_name)
# pad all other (unstable) phases with NaN
for phase_name in phase_compositions.keys():
if phase_name not in phase_compositions_accounted_for:
for comp in phase_compositions[phase_name].keys():
phase_compositions[phase_name][comp].append(np.nan)


def simulate_scheil_solidification(dbf, comps, phases, composition,
start_temperature, step_temperature=1.0,
liquid_phase_name='LIQUID', eq_kwargs=None,
Expand Down Expand Up @@ -109,10 +139,11 @@ def simulate_scheil_solidification(dbf, comps, phases, composition,
solid_phases = sorted((set(phases) | filtered_disordered_phases) - {liquid_phase_name})
temp = start_temperature
independent_comps = sorted([str(comp)[2:] for comp in composition.keys()])
x_liquid = {comp: [composition[v.X(comp)]] for comp in independent_comps}
pure_comps = sorted(set(comps) - {"VA"})
fraction_solid = [0.0]
temperatures = [temp]
phase_amounts = {ph: [0.0] for ph in solid_phases}
phase_compositions = {ph: {comp: [np.nan] for comp in pure_comps} for ph in sorted(set(solid_phases) | {liquid_phase_name})}

if adaptive:
dof_dict = {phase_name: list(map(len, mod.constituents)) for phase_name, mod in models.items()}
Expand Down Expand Up @@ -179,8 +210,8 @@ def simulate_scheil_solidification(dbf, comps, phases, composition,
liquid_comp = {}
for comp in independent_comps:
x = float(eq["X"].isel(vertex=liquid_vertex).squeeze().sel(component=comp).values)
x_liquid[comp].append(x)
liquid_comp[v.X(comp)] = x
_update_phase_compositions(phase_compositions, eq)
np_liq = np.nansum(eq.where(eq["Phase"] == liquid_phase_name).NP.values)
current_fraction_solid = float(fraction_solid[-1])
found_phase_amounts = [(liquid_phase_name, np_liq)] # tuples of phase name, amount
Expand Down Expand Up @@ -212,8 +243,7 @@ def simulate_scheil_solidification(dbf, comps, phases, composition,
temp -= step_temperature

if fraction_solid[-1] < 1:
for comp in independent_comps:
x_liquid[comp].append(np.nan)
_update_phase_compositions(phase_compositions, eq)
fraction_solid.append(1.0)
temperatures.append(temp)
# set the final phase amount to the phase fractions in the eutectic
Expand All @@ -225,7 +255,7 @@ def simulate_scheil_solidification(dbf, comps, phases, composition,
else:
phase_amounts[solid_phase].append(0.0)

return SolidificationResult(x_liquid, fraction_solid, temperatures, phase_amounts, converged, "scheil")
return SolidificationResult(phase_compositions, fraction_solid, temperatures, phase_amounts, converged, "scheil")


def simulate_equilibrium_solidification(dbf, comps, phases, composition,
Expand Down Expand Up @@ -267,6 +297,8 @@ def simulate_equilibrium_solidification(dbf, comps, phases, composition,
ordering_records = create_ordering_records(dbf, comps, phases)
filtered_disordered_phases = {ord_rec.disordered_phase_name for ord_rec in ordering_records}
solid_phases = sorted((set(phases) | filtered_disordered_phases) - {liquid_phase_name})
pure_comps = sorted(set(comps) - {"VA"})
phase_compositions = {ph: {comp: [] for comp in pure_comps} for ph in sorted(set(solid_phases) | {liquid_phase_name})}
independent_comps = sorted([str(comp)[2:] for comp in composition.keys()])
if 'model' not in eq_kwargs:
eq_kwargs['model'] = instantiate_models(dbf, comps, phases)
Expand Down Expand Up @@ -295,7 +327,6 @@ def simulate_equilibrium_solidification(dbf, comps, phases, composition,
eq_kwargs['calc_opts']['points'] = points_dict

temperatures = []
x_liquid = {comp: [] for comp in independent_comps}
fraction_solid = []
phase_amounts = {ph: [] for ph in solid_phases} # instantaneous phase amounts
cum_phase_amounts = {ph: [] for ph in solid_phases}
Expand All @@ -316,12 +347,11 @@ def simulate_equilibrium_solidification(dbf, comps, phases, composition,
if adaptive:
# Update the points dictionary with local samples around the equilibrium site fractions
_update_points(eq, eq_kwargs['calc_opts']['points'], dof_dict)
_update_phase_compositions(phase_compositions, eq)
if liquid_phase_name in eq.Phase:
# Add the liquid phase composition
# TODO: will break in a liquid miscibility gap
liquid_vertex = np.nonzero(eq.Phase == liquid_phase_name)[-1][0]
for comp in independent_comps:
x_liquid[comp].append(float(eq.X[..., liquid_vertex, eq.component.index(comp)]))
temperatures.append(current_T)
current_T -= step_temperature
else:
Expand Down Expand Up @@ -360,9 +390,6 @@ def simulate_equilibrium_solidification(dbf, comps, phases, composition,
if adaptive:
# Update the points dictionary with local samples around the equilibrium site fractions
_update_points(eq, eq_kwargs['calc_opts']['points'], dof_dict)
# Set the liquid phase composition to NaN
for comp in independent_comps:
x_liquid[comp].append(float(np.nan))

# Calculate fraction of solid and solid phase amounts
current_fraction_solid = 0.0
Expand All @@ -380,4 +407,4 @@ def simulate_equilibrium_solidification(dbf, comps, phases, composition,
fraction_solid.append(current_fraction_solid)

converged = True if np.isclose(fraction_solid[-1], 1.0) else False
return SolidificationResult(x_liquid, fraction_solid, temperatures, phase_amounts, converged, "equilibrium")
return SolidificationResult(phase_compositions, fraction_solid, temperatures, phase_amounts, converged, "equilibrium")
41 changes: 34 additions & 7 deletions scheil/solidification_result.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import numpy as np
import pandas as pd


class SolidificationResult():
"""Data from an equilibrium or Scheil-Gulliver solidification simulation.
Parameters
----------
x_liquid : Dict[str, List[float]]
phase_compositions : Mapping[PhaseName, Mapping[ComponentName, List[float]]]
Mapping of component name to composition at each temperature.
fraction_solid : List[float]
Fraction of solid at each temperature.
Expand All @@ -25,7 +26,7 @@ class SolidificationResult():
Attributes
----------
x_liquid : Dict[str, List[float]
phase_compositions : Mapping[PhaseName, Mapping[ComponentName, List[float]]]
fraction_solid : List[float]
temperatures : List[float]
phase_amounts : Dict[str, float]
Expand All @@ -38,13 +39,16 @@ class SolidificationResult():
"""

def __init__(self, x_liquid, fraction_solid, temperatures, phase_amounts, converged, method):
self.x_liquid = x_liquid
def __init__(self, phase_compositions, fraction_solid, temperatures, phase_amounts, converged, method):
# sort of a hack because we don't explictly track liquid phase name
self.phase_compositions = phase_compositions
self.fraction_solid = fraction_solid
self.fraction_liquid = (1.0 - np.array(fraction_solid)).tolist()
self.temperatures = temperatures
self.phase_amounts = phase_amounts
self.cum_phase_amounts = {ph: np.cumsum(amnts).tolist() for ph, amnts in phase_amounts.items()}
self.liquid_phase_name = list(set(self.phase_compositions.keys()) - set(self.cum_phase_amounts.keys()))[0]
self.x_liquid = phase_compositions[self.liquid_phase_name] # keeping for backwards compatibility, but this is also present in self.phase_compositions
self.converged = converged
self.method = method

Expand All @@ -56,7 +60,7 @@ def __repr__(self):

def to_dict(self):
d = {
'x_liquid': self.x_liquid,
'phase_compositions': self.phase_compositions,
'fraction_solid': self.fraction_solid,
'temperatures': self.temperatures,
'phase_amounts': self.phase_amounts,
Expand All @@ -67,10 +71,33 @@ def to_dict(self):

@classmethod
def from_dict(cls, d):
x_liquid = d['x_liquid']
phase_compositions = d['phase_compositions']
fraction_solid = d['fraction_solid']
temperatures = d['temperatures']
phase_amounts = d['phase_amounts']
converged = d['converged']
method = d['method']
return cls(x_liquid, fraction_solid, temperatures, phase_amounts, converged, method)
return cls(phase_compositions, fraction_solid, temperatures, phase_amounts, converged, method)

def to_dataframe(self, include_zero_phases=True):
"""
Parameters
----------
include_zero_phases : Optional[bool]
If True (the default), phases that never become stable in the simulation will be included.
"""
data_dict = {}
data_dict["Temperature (K)"] = self.temperatures
data_dict[f"NP({self.liquid_phase_name})"] = self.fraction_liquid
stable_phases = {self.liquid_phase_name}
for phase_name, vals in sorted(self.cum_phase_amounts.items()):
if vals[-1] > 0: # vals[-2] handles liquid case
stable_phases.add(phase_name)
if phase_name in stable_phases or include_zero_phases:
data_dict[f"NP({phase_name})"] = vals
for phase_name, phase_compositions in self.phase_compositions.items():
if phase_name in stable_phases or include_zero_phases:
for comp, vals in phase_compositions.items():
data_dict[f"X({phase_name},{comp})"] = vals
df = pd.DataFrame(data_dict)
return df
12 changes: 10 additions & 2 deletions tests/test_scheil_solidification.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ def test_scheil_solidification_result_properties():
assert rnd_trip_sol_res.converged == sol_res.converged
assert rnd_trip_sol_res.method == sol_res.method

# Test to_dataframe doesn't raise
sol_res.to_dataframe(include_zero_phases=True)
sol_res.to_dataframe(include_zero_phases=False)


def test_equilibrium_solidification_result_properties():
"""Test that SolidificationResult objects produced by equilibrium have the required properties."""
Expand All @@ -75,7 +79,7 @@ def test_equilibrium_solidification_result_properties():
assert num_temperatures == len(sol_res.fraction_liquid)
assert num_temperatures == len(sol_res.fraction_solid)
assert all([num_temperatures == len(np) for np in sol_res.phase_amounts.values()])
assert all([num_temperatures == len(liq_comps) for liq_comps in sol_res.x_liquid.values()])
assert all([num_temperatures == len(liq_comps) for liq_comps in sol_res.phase_compositions[sol_res.liquid_phase_name].values()])
assert all([num_temperatures == len(nphase) for nphase in sol_res.cum_phase_amounts.values()])
# The final cumulative solid phase amounts is 1.0
assert np.isclose(np.sum([amnts[-1] for amnts in sol_res.cum_phase_amounts.values()]), 1.0)
Expand All @@ -92,9 +96,13 @@ def test_equilibrium_solidification_result_properties():
rnd_trip_sol_res = SolidificationResult.from_dict(sol_res.to_dict())
assert rnd_trip_sol_res.fraction_liquid == sol_res.fraction_liquid
assert rnd_trip_sol_res.fraction_solid == sol_res.fraction_solid
assert rnd_trip_sol_res.x_liquid == sol_res.x_liquid
assert rnd_trip_sol_res.phase_compositions == sol_res.phase_compositions
assert rnd_trip_sol_res.cum_phase_amounts == sol_res.cum_phase_amounts
assert rnd_trip_sol_res.phase_amounts == sol_res.phase_amounts
assert rnd_trip_sol_res.temperatures == sol_res.temperatures
assert rnd_trip_sol_res.converged == sol_res.converged
assert rnd_trip_sol_res.method == sol_res.method

# Test to_dataframe doesn't raise
sol_res.to_dataframe(include_zero_phases=True)
sol_res.to_dataframe(include_zero_phases=False)

0 comments on commit 90a0e20

Please sign in to comment.