Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: improve handling for ordered phases #24

Merged
merged 2 commits into from
Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions scheil/ordering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""
Utilities for distinguishing and renaming ordered and disordered configurations of
multi-sublattice phases.

`OrderingRecord` objects are able to be used for any phase. `OrderingRecords` can be
created automatically for phases modeled with a partitioned order/disorder model through
the `create_ordering_records` method, since the partitioned model contains all the
information about the ordered and disordered phase.
"""

from dataclasses import dataclass
from typing import Sequence, List
import itertools
from collections import defaultdict
import numpy as np
import xarray as xr
from pycalphad.core.utils import unpack_components

@dataclass
class OrderingRecord:
ordered_phase_name: str
disordered_phase_name: str
subl_dof: Sequence[int] # number of degrees of freedom in each sublattice of the ordered phase
symmetric_subl_idx: Sequence[Sequence[int]] # List of sublattices (of the ordered phase) that are symmetric

def is_disordered(self, site_fractions):
# Short circuit if any site fraction is NaN (i.e. no phase or a different phase)
if np.any(np.isnan(site_fractions[:sum(self.subl_dof)])):
return False

# For each sublattice, create a `slice` object for slicing the site
# fractions of that particular sublattice from the site fraction array
subl_slices = []
for subl_idx in range(len(self.subl_dof)):
start_idx = np.sum(self.subl_dof[:subl_idx], dtype=np.int_)
end_idx = start_idx + self.subl_dof[subl_idx]
subl_slices.append(slice(start_idx, end_idx))

# For each set of symmetrically equivalent sublattices
for symm_subl in self.symmetric_subl_idx:
# Check whether the site fractions of each pair of symmetrically
# equivalent sublattices are ordered or disordered
for idx1, idx2 in itertools.combinations(symm_subl, 2):
# A phase is ordered if any pair of sublattices does not have
# equal (within numerical tolerance) site fractions
pair_is_ordered = np.any(~np.isclose(site_fractions[subl_slices[idx1]], site_fractions[subl_slices[idx2]]))
if pair_is_ordered:
return False
return True


def create_ordering_records(dbf, comps, phases):
"""Return a dictionary with the sublattice degrees of freedom and equivalent
sublattices for order/disorder phases

Parameters
----------
dbf : pycalphad.Database
comps : list[str]
List of active components to consider
phases : list[str]
List of active phases to consider

Returns
-------
List[OrderingRecord]

Notes
-----
Phases which should be checked for ordered/disordered configurations are
determined heuristically for this script.

The heuristic for a phase satisfies the following:
1. The phase is the ordered part of an order-disorder model
2. The equivalent sublattices have all the same number of elements
"""
species = unpack_components(dbf, comps)
ordering_records = []
for phase_name in phases:
phase_obj = dbf.phases[phase_name]
if phase_name == phase_obj.model_hints.get('ordered_phase', ''):
# This phase is active and modeled with an order/disorder model.
dof = [len(subl.intersection(species)) for subl in phase_obj.constituents]
# Define the symmetrically equivalent sublattices as any sublattices
# TODO: the heuristic here is simple and incorrect for cases like L1_2.
# that have the same site ratio. Create a {site_ratio: [subl idx]} dict
site_ratio_idxs = defaultdict(lambda: [])
for subl_idx, site_ratio in enumerate(phase_obj.sublattices):
site_ratio_idxs[site_ratio].append(subl_idx)
equiv_sublattices = list(site_ratio_idxs.values())
ordering_records.append(OrderingRecord(phase_name, phase_obj.model_hints['disordered_phase'], dof, equiv_sublattices))
return ordering_records


def rename_disordered_phases(eq_result, ordering_records):
"""
Modify an xarray Dataset to rename the ordered phase names to the disordered phase
names if the equilibrium configuration is disordered

Parameters
----------
eq_result : xarray.Dataset
order_disorder_dict : OrderingRecord
Output from scheil.utils.order_disorder_dict

Returns
-------
xrray.Dataset
Dataset modified in-place

Notes
-----
This function does _not_ change the site fractions array of the disordered
configurations to match the site fractions matching the internal degrees of freedom
of the disordered phase's constituents (although that should be possible).

Examples
--------
>>> from pycalphad import Database, equilibrium, variables as v
>>> from pycalphad.tests.datasets import AL_C_FE_B2_TDB
>>> dbf = Database(AL_C_FE_B2_TDB)
>>> comps = ['AL', 'FE', 'VA']
>>> phases = list(dbf.phases.keys())
>>> eq_res = equilibrium(dbf, comps, ['B2_BCC'], {v.P: 101325, v.T: 1000, v.N: 1, v.X('AL'): [0.1, 0.4]})
>>> ordering_records = create_ordering_records(dbf, comps, phases)
>>> eq_res.Phase.values.squeeze().tolist()
[['B2_BCC', '', ''], ['B2_BCC', '', '']]
>>> out_result = rename_disordered_phases(eq_res, ordering_records)
>>> eq_res.Phase.values.squeeze().tolist()
[['A2_BCC', '', ''], ['B2_BCC', '', '']]
"""

for ord_rec in ordering_records:
# Array indices matching phase with ordered phase name
mask = eq_result.Phase == ord_rec.ordered_phase_name
# disordered_mask is a boolean mask that is True if the element listed as an
# ordered phase is a disordered configuration. We want to broadcast over all
# dimensions except for internal_dof (we need all internal dof to determine if
# the site fractions are disordered). The `OrderingRecord.is_disordered` method
# is not vectorized (operates on 1D site fractions), so we use `vectorize=True`.
disordered_mask = xr.apply_ufunc(ord_rec.is_disordered, eq_result.where(mask).Y, input_core_dims=[['internal_dof']], vectorize=True)
# Finally, use `xr.where` to set the value of the phase name to the disordered
# phase everywhere the mask is true and use the existing value otherwise
eq_result['Phase'] = xr.where(disordered_mask, ord_rec.disordered_phase_name, eq_result.Phase)
return eq_result
18 changes: 10 additions & 8 deletions scheil/simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from pycalphad.core.calculate import _sample_phase_constitution
from pycalphad.core.utils import instantiate_models, unpack_components, filter_phases, point_sample
from .solidification_result import SolidificationResult
from .utils import order_disorder_dict, local_sample, order_disorder_eq_phases, get_phase_amounts
from .utils import local_sample, get_phase_amounts
from .ordering import create_ordering_records, rename_disordered_phases


def is_converged(eq):
Expand Down Expand Up @@ -95,14 +96,14 @@ def simulate_scheil_solidification(dbf, comps, phases, composition,
MAXIMUM_STEP_SIZE_REDUCTION = 5.0
T_STEP_ORIG = step_temperature
phases = filter_phases(dbf, unpack_components(dbf, comps), phases)
ord_disord_dict = order_disorder_dict(dbf, comps, phases)
ordering_records = create_ordering_records(dbf, comps, phases)
models = instantiate_models(dbf, comps, phases)
if verbose:
print('building PhaseRecord objects... ', end='')
phase_records = build_phase_records(dbf, comps, phases, [v.N, v.P, v.T], models)
if verbose:
print('done')
filtered_disordered_phases = {ord_ph_dict['disordered_phase'] for ord_ph_dict in ord_disord_dict.values()}
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})
temp = start_temperature
independent_comps = sorted([str(comp)[2:] for comp in composition.keys()])
Expand Down Expand Up @@ -140,8 +141,8 @@ def simulate_scheil_solidification(dbf, comps, phases, composition,
if adaptive:
_update_points(eq, eq_kwargs['calc_opts']['points'], dof_dict, verbose=verbose)
eq = eq.get_dataset() # convert LightDataset to Dataset for fancy indexing
eq_phases = order_disorder_eq_phases(eq, ord_disord_dict)
num_eq_phases = np.nansum(np.array([str(ph) for ph in eq_phases]) != '')
eq = rename_disordered_phases(eq, ordering_records)
eq_phases = eq.Phase.values.squeeze().tolist()
new_phases_seen = set(eq_phases).difference(phases_seen)
if len(new_phases_seen) > 0:
if verbose:
Expand Down Expand Up @@ -259,8 +260,8 @@ def simulate_equilibrium_solidification(dbf, comps, phases, composition,
"""
eq_kwargs = eq_kwargs or dict()
phases = filter_phases(dbf, unpack_components(dbf, comps), phases)
ord_disord_dict = order_disorder_dict(dbf, comps, phases)
filtered_disordered_phases = {ord_ph_dict['disordered_phase'] for ord_ph_dict in ord_disord_dict.values()}
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})
independent_comps = sorted([str(comp)[2:] for comp in composition.keys()])
models = instantiate_models(dbf, comps, phases)
Expand Down Expand Up @@ -357,7 +358,8 @@ def simulate_equilibrium_solidification(dbf, comps, phases, composition,

# Calculate fraction of solid and solid phase amounts
current_fraction_solid = 0.0
current_cum_phase_amnts = get_phase_amounts(order_disorder_eq_phases(eq.get_dataset(), ord_disord_dict), eq.NP.squeeze(), solid_phases)
eq = rename_disordered_phases(eq.get_dataset(), ordering_records)
current_cum_phase_amnts = get_phase_amounts(eq.Phase.values.squeeze(), eq.NP.squeeze(), solid_phases)
for solid_phase, amount in current_cum_phase_amnts.items():
# Since the equilibrium calculations always give the "cumulative" phase amount,
# we need to take the difference to get the instantaneous.
Expand Down
135 changes: 0 additions & 135 deletions scheil/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import itertools
from collections import defaultdict
import numpy as np
from scipy.stats import norm
from pycalphad.core.utils import unpack_components, generate_dof


def get_phase_amounts(eq_phases, phase_fractions, all_phases):
Expand All @@ -29,138 +26,6 @@ def get_phase_amounts(eq_phases, phase_fractions, all_phases):
return phase_amnts


def is_ordered(site_fracs, subl_dof, symmetric_subl_idx, **kwargs):
"""Return True if the site fraction configuration is ordered

Parameters
----------
site_fracs : numpy.ndarray[float,N]
Site fraction array for a phase of length sum(subl_dof) (can be padded
with arbitrary data, as returned by equilibrium calculations).
subl_dof : list[int]
List of the number of components active in each sublattice. Size should
be equivalent to the number of sublattices in the phase.
symmetric_subl_idx : list[list[int]]
List of sets of symmetrically equivalent sublattice indexes (as a list).
If sublattices at index 0 and index 1 are symmetric, as index 2 and
index 3, then the symmetric_subl_idx will be [[0, 1], [2, 3]].
kwargs :
Additional keyword arguments passed to ``np.isclose`` to check if site
fractions in symmetric sublattices are distinct.

Returns
-------
bool

Examples
--------
>>> dbf = Database('Fe-Ni-Ti.tdb') # doctest: +SKIP
>>> subl_dof = [4, 4, 1] # sublattice model is (Fe,Ni,Ti,Va)(Fe,Ni,Ti,Va)(Va) # doctest: +SKIP
>>> symm = [[0, 1]] # sublattices 0 and 1 should be equivalent # doctest: +SKIP
>>> res = equilibrium(dbf, ['FE', 'NI', 'TI', 'VA'], ['BCC2'], {v.P: 101325, v.T: 1200, v.N: 1, v.X('FE'): 0.001, v.X('NI'): 0.001}) # doctest: +SKIP
>>> is_ordered(res.Y.isel(vertex=0).values.squeeze(), subl_dof, symm) # doctest: +SKIP
False
>>> res = equilibrium(dbf, ['FE', 'NI', 'TI', 'VA'], ['BCC2'], {v.P: 101325, v.T: 1200, v.N: 1, v.X('FE'): 0.25, v.X('NI'): 0.25}) # doctest: +SKIP
>>> is_ordered(res.Y.isel(vertex=0).values.squeeze(), subl_dof, symm) # doctest: +SKIP
True

"""
# For each sublattice, create a ``slice`` object for slicing the site
# fractions of that particular sublattice from the site fraction array
subl_slices = []
for subl_idx in range(len(subl_dof)):
start_idx = np.sum(subl_dof[:subl_idx], dtype=np.int_)
end_idx = start_idx + subl_dof[subl_idx]
subl_slices.append(slice(start_idx, end_idx))

# For each set of symmetrically equivalent sublattices
for symm_subl in symmetric_subl_idx:
# Check whether the site fractions of each pair of symmetrically
# equivalent sublattices are ordered or disordered
for idx1, idx2 in itertools.combinations(symm_subl, 2):
# A phase is ordered if any pair of sublattices does not have
# equal (within numerical tolerance) site fractions
pair_is_ordered = np.any(~np.isclose(site_fracs[subl_slices[idx1]], site_fracs[subl_slices[idx2]], **kwargs))
if pair_is_ordered:
return True
return False


def order_disorder_dict(dbf, comps, phases):
"""Return a dictionary with the sublattice degrees of freedom and equivalent
sublattices for order/disorder phases

Parameters
----------
dbf : pycalphad.Database
comps : list[str]
List of active components to consider
phases : list[str]
List of active phases to consider

Returns
-------
dict

Notes
-----
Phases which should be checked for ordered/disordered configurations are
determined heuristically for this script.

The heuristic for a phase satisfies the following:
1. The phase is the ordered part of an order-disorder model
2. The equivalent sublattices have all the same number of elements
"""
species = unpack_components(dbf, comps)
ord_disord_phases = {}
for phase_name in phases:
phase_obj = dbf.phases[phase_name]
if phase_name == phase_obj.model_hints.get('ordered_phase', ''):
# This phase is active and modeled with an order/disorder model.
dof = generate_dof(dbf.phases[phase_name], species)[1]
# Define the symmetrically equivalent sublattices as any sublattices
# that have the same site ratio. Create a {site_ratio: [subl idx]} dict
site_ratio_idxs = defaultdict(lambda: [])
for subl_idx, site_ratio in enumerate(phase_obj.sublattices):
site_ratio_idxs[site_ratio].append(subl_idx)
equiv_sublattices = list(site_ratio_idxs.values())
ord_disord_phases[phase_name] = {
'subl_dof': dof,
'symmetric_subl_idx': equiv_sublattices,
'disordered_phase': phase_obj.model_hints['disordered_phase']
}
return ord_disord_phases


def order_disorder_eq_phases(eq_result, order_disorder_dict):
"""Return a list corresponding to the eq_result.Phase with order/disorder
phases named correctly.

Parameters
----------
eq_result : pycalphad.LightDataset
order_disorder_dict : Dict

Returns
-------
List
"""
eq_phases = []
for vtx in eq_result.vertex.values:
eq_phase = str(eq_result["Phase"].isel(vertex=vtx).values.squeeze())
site_fracs = eq_result["Y"].isel(vertex=vtx).values.squeeze()
if eq_phase in order_disorder_dict:
odd = order_disorder_dict[eq_phase]
is_ord = is_ordered(site_fracs, odd['subl_dof'], odd['symmetric_subl_idx'])
if is_ord:
eq_phases.append(eq_phase)
else:
eq_phases.append(odd['disordered_phase'])
else:
eq_phases.append(eq_phase)
return eq_phases


def local_sample(sitefracs, comp_count, pdens=100, stddev=0.05):
"""Sample from a normal distribution around the optimal site fractions

Expand Down