Skip to content

Commit

Permalink
MAINT: Refactor callables creation in equilibrium() and calculate() (g…
Browse files Browse the repository at this point in the history
…h-192)

Fixes gh-189.
- Creates a new codegen subdirectory; move some existing codegen functionality there
- New build_callables function
FIX: equilibrium/calculate: Properly filter phases that cannot be built with build_callables()
MAINT: Remove Hessian callable support and clean up PhaseRecord pickling
FIX: solver: Tweak to attempt fix for stochastic failure in test_eq_issue43_chempots_misc_gap
BLD: appveyor: Build fixes
BLD: New implicit dependency on numpy>=1.13 from xarray
MAINT: dask>=0.18 compatibility
MAINT: core.utils: Add get_pure_elements from ESPEI
MAINT: codegen: Move sympydiff_utils and custom_autowrap to separate subdirectory
  • Loading branch information
richardotis committed Nov 13, 2018
1 parent 5ad8830 commit 9a2a907
Show file tree
Hide file tree
Showing 22 changed files with 411 additions and 299 deletions.
5 changes: 2 additions & 3 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ install:
# Check that we have the expected version and architecture for Python
- "python --version"
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
- "conda install --yes --quiet conda=4.3.30"
- "conda install --yes --quiet conda=4.3.31"
- "conda config --system --add pinned_packages defaults::conda"
- "conda -V"
- "conda create --yes -n condaenv python=%PYTHON_VERSION%"
Expand All @@ -38,8 +38,7 @@ install:
- "conda config --add channels msys2"
- "conda config --add channels pycalphad"
# install pycalphad and dependencies
- "conda install --yes -n condaenv --quiet pip setuptools nose numpy pandas scipy sympy pyparsing matplotlib xarray dask dill"
- "conda install --yes -n condaenv --quiet tinydb cython cyipopt m2w64-toolchain libpython"
- "conda install --yes -n condaenv --quiet pip setuptools nose numpy pandas scipy sympy pyparsing matplotlib xarray dask dill tinydb cython cyipopt m2w64-toolchain libpython"
- "pip install -e ."

build: false
Expand Down
8 changes: 4 additions & 4 deletions conda_recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ requirements:
- pyparsing
- tinydb
- scipy
- dask >=0.15
- dask >=0.18
- distributed
- numpy >=1.9
- numpy >=1.13
- dill
- cython >=0.24
- cyipopt
Expand All @@ -49,9 +49,9 @@ requirements:
- pyparsing
- tinydb
- scipy
- dask >=0.15
- dask >=0.18
- distributed
- numpy >=1.9
- numpy >=1.13
- cython >=0.24
- dill
- cyipopt
Expand Down
2 changes: 1 addition & 1 deletion pycalphad/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import pycalphad.core.patched_piecewise
sympy.functions.elementary.piecewise.Piecewise.eval = classmethod(pycalphad.core.patched_piecewise.piecewise_eval)

from pycalphad.core.errors import *
import pycalphad.variables as v
from pycalphad.model import Model
from pycalphad.io.database import Database
Expand All @@ -35,7 +36,6 @@

from pycalphad.core.calculate import calculate
from pycalphad.core.equilibrium import equilibrium
from pycalphad.core.equilibrium import EquilibriumError, ConditionError
from pycalphad.plot.binary import binplot
from pycalphad.plot.ternary import ternplot
from pycalphad.plot.eqplot import eqplot
Expand Down
Empty file added pycalphad/codegen/__init__.py
Empty file.
171 changes: 171 additions & 0 deletions pycalphad/codegen/callables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import pycalphad.variables as v
from pycalphad import Model
from pycalphad.codegen.sympydiff_utils import build_functions
from pycalphad.core.utils import get_pure_elements, unpack_components, unpack_kwarg
from pycalphad.core.phase_rec import PhaseRecord_from_cython
from sympy import Symbol
import numpy as np
import operator
from itertools import repeat
from collections import defaultdict


def wrap_symbol(obj):
if isinstance(obj, Symbol):
return obj
else:
return Symbol(obj)

def build_callables(dbf, comps, phases, model=None, parameters=None, callables=None,
output='GM', build_gradients=True, verbose=False):
"""
Create dictionaries of callable dictionaries and PhaseRecords.
Parameters
----------
dbf : Database
A Database object
comps : list
List of component names
phases : list
List of phase names
model : dict or type
Dictionary of {phase_name: Model subclass} or a type corresponding to a
Model subclass. Defaults to ``Model``.
parameters : dict, optional
Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database.
callables : dict, optional
Pre-computed callables
output : str
Output property of the particular Model to sample
build_gradients : bool
Whether or not to build gradient functions. Defaults to True.
verbose : bool
Print the name of the phase when its callables are built
Returns
-------
callables : dict
Dictionary of keyword argument callables to pass to equilibrium.
Example
-------
>>> dbf = Database('AL-NI.tdb')
>>> comps = ['AL', 'NI', 'VA']
>>> phases = ['FCC_L12', 'BCC_B2', 'LIQUID', 'AL3NI5', 'AL3NI2', 'AL3NI']
>>> callables = build_callables(dbf, comps, phases)
>>> equilibrium(dbf, comps, phases, conditions, **callables)
"""
parameters = parameters if parameters is not None else {}
if len(parameters) > 0:
param_symbols, param_values = zip(*[(key, val) for key, val in sorted(parameters.items(),
key=operator.itemgetter(0))])
param_values = np.asarray(param_values, dtype=np.float64)
else:
param_symbols = []
param_values = np.empty(0)
comps = sorted(unpack_components(dbf, comps))
pure_elements = get_pure_elements(dbf, comps)

callables = callables if callables is not None else {}
_callables = {
'massfuncs': {},
'massgradfuncs': {},
'callables': {},
'grad_callables': {}
}

models = unpack_kwarg(model, default_arg=Model)
param_symbols = [wrap_symbol(sym) for sym in param_symbols]
phase_records = {}
# create models
for name in phases:
mod = models[name]
if isinstance(mod, type):
models[name] = mod = mod(dbf, comps, name, parameters=param_symbols)
site_fracs = mod.site_fractions
variables = sorted(site_fracs, key=str)
try:
out = getattr(mod, output)
except AttributeError:
raise AttributeError('Missing Model attribute {0} specified for {1}'
.format(output, mod.__class__))

if callables.get('callables', {}).get(name, False) and \
((not build_gradients) or callables.get('grad_callables',{}).get(name, False)):
_callables['callables'][name] = callables['callables'][name]
if build_gradients:
_callables['grad_callables'][name] = callables['grad_callables'][name]
else:
_callables['grad_callables'][name] = None
else:
# Build the callables of the output
# Only force undefineds to zero if we're not overriding them
undefs = {x for x in out.free_symbols if not isinstance(x, v.StateVariable)} - set(param_symbols)
undef_vals = repeat(0., len(undefs))
out = out.xreplace(dict(zip(undefs, undef_vals)))
build_output = build_functions(out, tuple([v.P, v.T] + site_fracs), parameters=param_symbols,
include_grad=build_gradients)
if build_gradients:
cf, gf = build_output
else:
cf = build_output
gf = None
_callables['callables'][name] = cf
_callables['grad_callables'][name] = gf

if callables.get('massfuncs', {}).get(name, False) and \
((not build_gradients) or callables.get('massgradfuncs', {}).get(name, False)):
_callables['massfuncs'][name] = callables['massfuncs'][name]
if build_gradients:
_callables['massgradfuncs'][name] = callables['massgradfuncs'][name]
else:
_callables['massgradfuncs'][name] = None
else:
# Build the callables for mass
# TODO: In principle, we should also check for undefs in mod.moles()

if build_gradients:
mcf, mgf = zip(*[build_functions(mod.moles(el), [v.P, v.T] + variables,
include_obj=True,
include_grad=build_gradients,
parameters=param_symbols)
for el in pure_elements])
else:
mcf = tuple([build_functions(mod.moles(el), [v.P, v.T] + variables,
include_obj=True,
include_grad=build_gradients,
parameters=param_symbols)
for el in pure_elements])
mgf = None
_callables['massfuncs'][name] = mcf
_callables['massgradfuncs'][name] = mgf
if not callables.get('phase_records', {}).get(name, False):
pv = param_values
else:
# Copy parameter values from old PhaseRecord, if it exists
pv = callables['phase_records'][name].parameters
phase_records[name.upper()] = PhaseRecord_from_cython(comps, variables,
np.array(dbf.phases[name].sublattices,
dtype=np.float),
pv,
_callables['callables'][name],
_callables['grad_callables'][name],
_callables['massfuncs'][name],
_callables['massgradfuncs'][name])
if verbose:
print(name + ' ')

# Update PhaseRecords with any user-specified parameter values, in case we skipped the build phase
# We assume here that users know what they are doing, and pass compatible combinations of callables and parameters
# See discussion in gh-192 for details
if len(param_values) > 0:
for prx_name in phase_records:
if len(phase_records[prx_name].parameters) != len(param_values):
raise ValueError('User-specified callables and parameters are incompatible')
phase_records[prx_name].parameters = param_values
# finally, add the models to the callables
_callables['model'] = dict(models)
_callables['phase_records'] = phase_records
return _callables
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""
This module constructs gradient functions for Models.
"""
from .custom_autowrap import autowrap, import_extension
from .cache import cacheit
from sympy import zoo, oo, ImmutableMatrix, IndexedBase, MatrixSymbol, Symbol, Idx, Dummy, Lambda, Eq, S
import numpy as np
from pycalphad.codegen.custom_autowrap import autowrap, import_extension
from pycalphad.core.cache import cacheit
from sympy import zoo, oo, ImmutableMatrix, IndexedBase, MatrixSymbol, Symbol, Idx, Lambda, Eq, S
import time
import tempfile
from threading import RLock
Expand Down
100 changes: 27 additions & 73 deletions pycalphad/core/calculate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,17 @@

from __future__ import division
from pycalphad import Model
from pycalphad.model import DofError
from pycalphad.core.sympydiff_utils import build_functions
from pycalphad.codegen.callables import build_callables
from pycalphad import ConditionError
from pycalphad.core.utils import point_sample, generate_dof
from pycalphad.core.utils import endmember_matrix, unpack_kwarg
from pycalphad.core.utils import broadcast_to, unpack_condition, unpack_phases, unpack_components
from pycalphad.core.utils import broadcast_to, filter_phases, unpack_condition, unpack_components
from pycalphad.core.cache import cacheit
from pycalphad.core.phase_rec import PhaseRecord, PhaseRecord_from_cython
from pycalphad.core.phase_rec import PhaseRecord
import pycalphad.variables as v
from sympy import Symbol
import numpy as np
import itertools
import collections
import warnings
from xarray import Dataset, concat
from collections import OrderedDict

Expand Down Expand Up @@ -299,15 +297,12 @@ def calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, bro
pdens_dict = unpack_kwarg(kwargs.pop('pdens', 2000), default_arg=2000)
points_dict = unpack_kwarg(kwargs.pop('points', None), default_arg=None)
model_dict = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model)
callable_dict = unpack_kwarg(kwargs.pop('callables', None), default_arg=None)
mass_dict = unpack_kwarg(kwargs.pop('massfuncs', None), default_arg=None)
callables_dict = kwargs.pop('callables', {})
sampler_dict = unpack_kwarg(kwargs.pop('sampler', None), default_arg=None)
fixedgrid_dict = unpack_kwarg(kwargs.pop('grid_points', True), default_arg=True)
parameters = parameters or dict()
if isinstance(parameters, dict):
parameters = OrderedDict(sorted(parameters.items(), key=str))
param_symbols = tuple(parameters.keys())
param_values = np.atleast_1d(np.array(list(parameters.values()), dtype=np.float))
if isinstance(phases, str):
phases = [phases]
if isinstance(comps, (str, v.Species)):
Expand All @@ -330,76 +325,35 @@ def calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, bro
str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \
for (key, value) in statevar_dict.items())
all_phase_data = []
comp_sets = {}
largest_energy = 1e30
maximum_internal_dof = 0

# Consider only the active phases
active_phases = dict((name.upper(), dbf.phases[name.upper()]) \
for name in unpack_phases(phases))
list_of_possible_phases = filter_phases(dbf, comps)
active_phases = sorted(set(list_of_possible_phases).intersection(set(phases)))
active_phases = {name: dbf.phases[name] for name in active_phases}
if len(list_of_possible_phases) == 0:
raise ConditionError('There are no phases in the Database that can be active with components {0}'.format(comps))
if len(active_phases) == 0:
raise ConditionError('None of the passed phases ({0}) are active. List of possible phases: {1}.'
.format(phases, list_of_possible_phases))

for phase_name, phase_obj in sorted(active_phases.items()):
# Build the symbolic representation of the energy
mod = model_dict[phase_name]
# if this is an object type, we need to construct it
if isinstance(mod, type):
try:
model_dict[phase_name] = mod = mod(dbf, comps, phase_name, parameters=parameters)
except DofError:
# we can't build the specified phase because the
# specified components aren't found in every sublattice
# we'll just skip it
warnings.warn("""Suspending specified phase {} due to
some sublattices containing only unspecified components""".format(phase_name))
continue
if points_dict[phase_name] is None:
maximum_internal_dof = max(maximum_internal_dof, sum(len(x) for x in mod.constituents))
else:
maximum_internal_dof = max(maximum_internal_dof, np.asarray(points_dict[phase_name]).shape[-1])
if isinstance(output, (list, tuple, set)):
raise NotImplementedError('Only one property can be specified in calculate() at a time')
output = output if output is not None else 'GM'
eq_callables = build_callables(dbf, comps, active_phases, model=model_dict,
parameters=parameters,
output=output, callables=callables_dict, build_gradients=False,
verbose=False)

phase_records = eq_callables['phase_records']
models = eq_callables['model']
maximum_internal_dof = max(len(mod.site_fractions) for mod in models.values())

for phase_name, phase_obj in sorted(active_phases.items()):
try:
mod = model_dict[phase_name]
except KeyError:
continue
# this is a phase model we couldn't construct for whatever reason; skip it
if isinstance(mod, type):
continue
# Construct an ordered list of the variables
variables, sublattice_dof = generate_dof(phase_obj, mod.components)
# Build the "fast" representation of that model
if callable_dict[phase_name] is None:
try:
out = getattr(mod, output)
except AttributeError:
raise AttributeError('Missing Model attribute {0} specified for {1}'
.format(output, mod.__class__))
# As a last resort, treat undefined symbols as zero
# But warn the user when we do this
# This is consistent with TC's behavior
undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable))
for undef in undefs:
out = out.xreplace({undef: float(0)})
warnings.warn('Setting undefined symbol {0} for phase {1} to zero'.format(undef, phase_name))
comp_sets[phase_name] = build_functions(out, list(statevar_dict.keys()) + variables,
include_obj=True, include_grad=False,
parameters=param_symbols)
else:
comp_sets[phase_name] = callable_dict[phase_name]
if mass_dict[phase_name] is None:
pure_elements = [spec for spec in nonvacant_components
if (len(spec.constituents.keys()) == 1 and
list(spec.constituents.keys())[0] == spec.name)
]
# TODO: In principle, we should also check for undefs in mod.moles()
mass_dict[phase_name] = [build_functions(mod.moles(el), list(statevar_dict.keys()) + variables,
include_obj=True, include_grad=False,
parameters=param_symbols)
for el in pure_elements]
phase_record = PhaseRecord_from_cython(comps, list(statevar_dict.keys()) + variables,
np.array(dbf.phases[phase_name].sublattices, dtype=np.float),
param_values, comp_sets[phase_name], None, None, mass_dict[phase_name], None)
mod = models[phase_name]
phase_record = phase_records[phase_name]
points = points_dict[phase_name]
variables, sublattice_dof = generate_dof(phase_obj, mod.components)
if points is None:
points = _sample_phase_constitution(phase_name, phase_obj.constituents, sublattice_dof, comps,
tuple(variables), sampler_dict[phase_name] or point_sample,
Expand Down
Loading

0 comments on commit 9a2a907

Please sign in to comment.