#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2024 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
from abc import abstractmethod
from itertools import product
from pyomo.core.base.param import SimpleParam
from pyomo.opt.results import SolutionStatus
from .pyomo_modeling import *
from ..materials.design import Design
class IndexedElem(object):
"""Base class for indexed MatOpt objects.
Should not be necessary for users to instantiate, but developers will
utilize the constructors (especially fromComb) and mask method when
creating new classes of Expression and Rule objects.
Attributes:
sites (list<int>): The sites that the object is indexed over
bonds (list<int>): The bonds that the object is indexed over
site_types (list<int>): The site types that the object is indexed over
bond_types (list<int>): The bond types that the object is indexed over
confs (list<int>): The conformations that the object is indexed over
"""
# === STANDARD CONSTRUCTOR
def __init__(
self, sites=None, bonds=None, site_types=None, bond_types=None, confs=None
):
"""Standard constructor of IndexedElem.
Args:
sites (list<int>): The sites that the object is indexed over
bonds (list<tuple<int,int>>): The bonds that the object is indexed
over
site_types (list<BBlock>): The site types that the object is indexed
over
bond_types (list<tuple<BBlock,BBlock>>): The bond types that the
object is indexed over
confs (list<int>): The conformations that the object is indexed over
"""
self._sites = sites
self._bonds = bonds
self._site_types = site_types
self._bond_types = bond_types
self._confs = confs
# === CONSTRUCTOR - From combinations of other IndexedElem objects
@classmethod
def fromComb(cls, *args):
"""
Constructor of IndexedElem from other IndexedElem objects.
This constructor creates a new indexing object by combining two
other indexed objects. The various attributes are mixed according
to three simple cases for each type of index:
Case #1: Both objects are indexed over that index.
The intersection of the two lists of indices is used.
Case #2: Only one object has that index.
The resulting object gains the index.
Case #3: Both objects are not indexed over that index.
The resulting object is also not indexed over that index.
This is simply meant to reproduce the way we use set notation for
writing products of expressions and variables in constraints.
Args:
*args (list<IndexedElem>): Objects to combine indices for.
Returns:
(IndexedElem) Index object from combination of indices.
"""
LHS, RHS, *args = args
Comb = cls._fromComb2(LHS, RHS)
if len(args) > 0:
return cls.fromComb(Comb, *args)
else:
return Comb
# === AUXILIARY METHODS
@classmethod
def _fromComb2(cls, LHS, RHS):
"""Constructor of an IndexedElem from two other IndexedElem objects.
Utilized in the recursive fromComb method, above.
Args:
LHS (IndexedElem): Object to combine indices for.
RHS (IndexedElem): Object to combine indices for.
Returns:
(IndexedElem) Index object from combination of indices.
"""
if LHS.sites is not None and RHS.sites is not None:
sites = list(set(LHS.sites) & set(RHS.sites))
else:
sites = LHS.sites if LHS.sites is not None else RHS.sites
if LHS.bonds is not None and RHS.bonds is not None:
bonds = list(set(LHS.bonds) & set(RHS.bonds))
else:
bonds = LHS.bonds if LHS.bonds is not None else RHS.bonds
if LHS.site_types is not None and RHS.site_types is not None:
site_types = list(set(LHS.site_types) & set(RHS.site_types))
else:
site_types = (
LHS.site_types if LHS.site_types is not None else RHS.site_types
)
if LHS.bond_types is not None and RHS.bond_types is not None:
bond_types = list(set(LHS.bond_types) & set(RHS.bond_types))
else:
bond_types = (
LHS.bond_types if LHS.bond_types is not None else RHS.bond_types
)
if LHS.confs is not None and RHS.confs is not None:
confs = list(set(LHS.confs) & set(RHS.confs))
else:
confs = LHS.confs if LHS.confs is not None else RHS.confs
return cls(
sites=sites,
bonds=bonds,
site_types=site_types,
bond_types=bond_types,
confs=confs,
)
def mask(self, index, Comb):
"""
Method to identify the indexes relevant to this object.
Given an instance of index that was generated by another IndexedElem
object (Comb), we identify which parts of that index were relevant
to this object.
Example::
VarIndexes = IndexedElem(sites=[1,2])
CoefIndexes = IndexedElem(site_types=['A','B'])
Comb = IndexedElem.fromComb(VarIndexes,CoefIndexes)
for k in Comb.keys():
site = VarIndexes.mask(k,Comb)
site_type = CoefIndexes.mask(k,Comb)
Args:
index (tuple<int/BBlock>): index from which to identify relevant parts
Comb (IndexedElem): object from which the index was generated
Returns:
(tuple<int/BBlock>) index with indices relevant to this object remaining
"""
if Comb.sites is not None:
i, *index = index
if Comb.bonds is not None:
i, j, *index = index
if Comb.site_types is not None:
k, *index = index
if Comb.bond_types is not None:
k, l, *index = index
if Comb.confs is not None:
c, *index = index
result = []
if self.sites is not None:
result.append(i)
if self.bonds is not None:
result.append(i)
result.append(j)
if self.site_types is not None:
result.append(k)
if self.bond_types is not None:
result.append(k)
result.append(l)
if self.confs is not None:
result.append(c)
if not result:
result = [None]
return tuple(result)
@property
def dims(self):
"""Relevant dimensions of indices.
Returns:
(list<bool>) flags to indicate which index sets are relevant.
"""
return (
self.sites is not None,
self.bonds is not None,
self.site_types is not None,
self.bond_types is not None,
self.confs is not None,
)
@property
def index_sets(self):
"""Sets (actually lists) of indices.
Note that in the cases that there are no relevant indices, a
dummy list [[None]] is returned to allow the [None] key to be
included.
Returns:
(list<list<int/BBlock>>) lists of indices of each relevant type.
"""
result = [
s
for s in (
self.sites,
self.bonds,
self.site_types,
self.bond_types,
self.confs,
)
if s is not None
]
if not result:
result = [[None]]
return result
@property
def index_dict(self):
"""Dictionary of relevant attributes.
Returns:
(dict<string:list<int/BBlock>>) attributes of this IndexedElem.
"""
return {
"sites": self.sites,
"bonds": self.bonds,
"site_types": self.site_types,
"bond_types": self.bond_types,
"confs": self.confs,
}
@property
def sites(self):
"""List of sites relevant to this object."""
return self._sites
@property
def bonds(self):
"""List of bonds relevant to this object."""
return self._bonds
@property
def site_types(self):
"""List of site types relevant to this object."""
return self._site_types
@property
def bond_types(self):
"""List of bond types relevant to this object."""
return self._bond_types
@property
def confs(self):
"""List of conformation types relevant to this object."""
return self._confs
def keys(self):
"""Method creating a generator for the keys relevant to this object.
Note that the [None] key is returned in the case of an object
not indexed by any of the possible index types. In other words,
[None] is the key for a scalar variable/expression/coefficient.
Returns:
(generator<tuple<int/BBlock>>) keys generator (similar to dict.keys()).
"""
index_sets = self.index_sets
if len(index_sets) > 1:
return product(*self.index_sets)
elif len(index_sets) == 1:
return (k for k in index_sets[0])
else:
raise NotImplementedError("There should always be at least " "a [None] key")
class Coef(IndexedElem):
"""A class for coefficient data indexed over material index sets.
This class is useful for representing indexed data and automatically
generating complex indexed expressions. For example, the multiplication
of bond-type coefficients with bond-indexed variables would not be
easily representable via standard Python objects.
The key benefit is that these objects have the useful IndexedElem methods
and also allow access of data via getitem operators.
Attributes:
vals (dict/list) data structure of coefficient values.
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, vals, **kwargs):
"""Standard constructor of indexed coefficients.
Args:
vals (list<float>/dict/other): Any data structure that supports the
__getitem__ method for keys generated by this object's
IndexedElem.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.vals = vals
IndexedElem.__init__(self, **kwargs)
# === BASIC QUERY METHODS
def __getitem__(self, k):
"""Method to access coefficient values by bracket operator"""
return self.vals[k]
class Expr(IndexedElem):
"""An abstract class for representing expressions when building rules.
The key benefit of this class is that we utilize the useful methods of
IndexedElem and establish the interface for derived expressions.
Expressions can be generated over a subset of the design space
(i.e., only for some combinations of sites, site-types, etc.) by
providing keywords that are passed to the constructor of IndexedElem.
Else, the relevant indexes are inferred from the expression components.
Attributes:
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, **kwargs):
"""Standard constructor for abstract class Expr.
Args:
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
IndexedElem.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
@abstractmethod
def _pyomo_expr(self, index=None):
"""Abstract interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
raise NotImplementedError
class LinearExpr(Expr):
"""A class for representing simple expressions of site descriptors.
The use of this class is to generate expressions from multiplication and
summation of coefficients and descriptors. Importantly, expressions
of this type maintain the same indexing of their component descriptors
and coefficients. Summation is taken across multiple descriptors, not
multiple instances of indexes.
Attributes:
coefs (float/list<float>): coefficient to multiply each descriptor by
descs (Descriptor/list<Descriptor>): descriptors to add together
offset (float/int): scalar value to add to the expression
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, descs=None, coefs=1.0, offset=0.0, **kwargs):
"""Standard constructor for linear expression objects.
Args:
coefs (float/list<float>): Optional, coefficient to multiply
each descriptor by. Default: 1.0
descs (Descriptor/list<Descriptor>): descriptors to add
offset (float/int): Optional, scalar value to add to the expression
Default: 0.0
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.coefs = coefs if type(coefs) is list else [coefs]
self.descs = descs if type(descs) is list else [descs]
self.offset = offset
if descs is not None:
if type(descs) is list:
Comb = IndexedElem.fromComb(*descs)
kwargs = {**Comb.index_dict, **kwargs}
else:
kwargs = {**descs.index_dict, **kwargs}
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
result = self.offset
for it, desc in enumerate(self.descs):
if desc is not None and self.coefs[it] is not None:
result += self.coefs[it] * desc._pyomo_var[index]
return result
class SiteCombination(Expr):
"""A class for representing summations of descriptors at two sites.
Attributes:
coefi (float/list<float>): coefficients at the first site
desci (Descriptor/Expr): descriptor or expression for the first site
coefj (float/list<float>): coefficients at the second site
descj (Descriptor/Expr): descriptor or expression for the second site
offset (float): scalar coefficient to add to the rest of the expression
symmetric_bonds (bool): flag to indicate if site combinations should be
considered symmetric (and therefore, should only generate half as
many terms) (index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(
self,
coefi,
desci,
coefj=None,
descj=None,
offset=0.0,
symmetric_bonds=False,
**kwargs
):
"""Standard constructor for site combination expressions.
Args:
coefi (float/list<float>): coefficients at the first site.
Default: 1.0
desci (Descriptor/Expr): term for first site
coefj (float/list<float>): Optional, coefficients at the second site.
Default: Equal to the coefficient for the first site.
descj (Descriptor/Expr): Optional, term for the second site.
Default: Equal to the descriptor for the first site.
offset (float): Optional, scalar coefficient to add to expression.
Default: 1.0
symmetric_bonds (bool): Optional, flag to indicate if combinations
are symmetric.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.coefi = coefi
self.desci = desci
self.coefj = coefj if coefj is not None else coefi
self.descj = descj if descj is not None else desci
self.offset = offset
self.symmetric_bonds = symmetric_bonds
if "bonds" not in kwargs:
kwargs["bonds"] = [
(i, j)
for i in desci.sites
for j in desci.canv.NeighborhoodIndexes[i]
if (j is not None and (not symmetric_bonds or j > i))
]
if "bond_types" in kwargs:
pass # use the kwargs bond_types
elif coefi.bond_types is not None:
kwargs["bond_types"] = coefi.bond_types
elif desci.site_types is not None:
kwargs["bond_types"] = [
(k, l) for k in desci.site_types for l in desci.site_types
]
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
if len(index) == 4:
i, j, k, l = index
elif len(index) == 2:
i, j, k, l = index[0], index[1], (), ()
else:
raise NotImplementedError(
"Decide how to split the extra " "indices in this case..."
)
if (
type(self.coefi) is float
or type(self.coefi) is int
or type(self.coefi) is SimpleParam
):
ci = self.coefi
else:
coefi_index = self.coefi.mask(
(i, i, j, k, k, l),
IndexedElem(
sites=[i], bonds=[(i, j)], site_types=[k], bond_types=[(k, l)]
),
)
ci = self.coefi[coefi_index]
if (
type(self.coefj) is float
or type(self.coefj) is int
or type(self.coefj) is SimpleParam
):
cj = self.coefj
else:
coefj_index = self.coefj.mask(
(j, j, i, l, l, k),
IndexedElem(
sites=[j], bonds=[(j, i)], site_types=[l], bond_types=[(l, k)]
),
)
cj = self.coefj[coefj_index]
desci_index = self.desci.mask(
(i, i, j, k, k, l),
IndexedElem(sites=[i], bonds=[(i, j)], site_types=[k], bond_types=[(k, l)]),
)
descj_index = self.descj.mask(
(j, j, i, l, l, k),
IndexedElem(sites=[j], bonds=[(j, i)], site_types=[l], bond_types=[(l, k)]),
)
di = self.desci._pyomo_expr(index=desci_index)
dj = self.descj._pyomo_expr(index=descj_index)
return self.offset + ci * di + cj * dj
class SumNeighborSites(Expr):
"""A class for expressions for summation across neighbor sites.
Attributes:
desc (Descriptor): descriptors to sum around a site
coefs (float/list<float>): Optional, coefficients to multiple each
neighbor term by. Default=1.0
offset: Optional, term to add to the expression.
Default=0.0 (index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, desc, coefs=1.0, offset=0.0, **kwargs):
"""Standard constructor for expressions of neighbor summations.
Args:
desc (Descriptor): descriptors to sum around a site
coefs (float/list<float>): Optional, coefficients to multiple each
neighbor term by. Default=1.0
offset: Optional, term to add to the expression.
Default=0.0
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.desc = desc
self.coefs = coefs
self.offset = offset
kwargs = {**desc.index_dict, **kwargs}
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
i, *index = index
if index == (None,):
index = ()
result = self.offset
for n, j in enumerate(self.desc.canv.NeighborhoodIndexes[i]):
if j is not None:
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[n]
) * self.desc._pyomo_var[(j, *index)]
return result
class SumNeighborBonds(Expr):
"""A class for expressions from summation of neighbor bond descriptors.
Attributes:
desc (Descriptor/Expr): descriptors to sum over
coefs (float/list<float>): coefficients to multiply bonds to neighbor sites
offset (float): coefficient to add to the expression
symmetric_bonds (bool): flag to indicate if bond variables should be
added in a symmetric way (index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, desc, coefs=1.0, offset=0.0, symmetric_bonds=False, **kwargs):
"""Standard constructor for summation of neighboring bonds
Args:
desc (Descriptor): descriptors to sum around a site
coefs (float/list<float>): Optional, coefficients to multiple each
neighbor term by. Default=1.0
offset (float): Optional, coefficient to add to the expression.
Default=0.0
symmetric_bonds (bool): Optional, flag to indicate if bond variables
should be considered symmetric.
Default=False
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.desc = desc
self.coefs = coefs
self.offset = offset
self.symmetric_bonds = symmetric_bonds
kwargs = {**desc.index_dict, **kwargs}
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
i, *index = index
if index == (None,):
index = ()
result = self.offset
for n, j in enumerate(self.desc.canv.NeighborhoodIndexes[i]):
if j is not None:
if self.symmetric_bonds:
i, j = min(i, j), max(i, j)
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[n]
) * self.desc._pyomo_expr(index=(i, j, *index))
return result
class SumSites(Expr):
"""A class for expressions formed by summation over canvas sites.
Attributes:
desc (Descriptor/Expr): descriptors or expressions to sum over
coefs (float/list<float>): coefficients to multiply contributions
from each site
offset (float): coefficient to add to the expression
sites_to_sum (list<int>): sites to consider in the summation
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, desc, coefs=1.0, offset=0.0, sites_to_sum=None, **kwargs):
"""Standard constructor for summation of site contributions.
Args:
desc (Descriptor): descriptors or expressions to sum across all sites
coefs (float/list<float>): Optional, coefficients to multiple each
site term by. Default=1.0
offset (float): Optional, coefficient to add to the expression.
Default=0.0
sites_to_sum (list<int>): Optional, subset of canvas sites to sum.
Default=None, meaning all sites in the desc object are considered.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.desc = desc
self.coefs = coefs
self.offset = offset
self.sites_to_sum = sites_to_sum if sites_to_sum is not None else desc.sites
kwargs = {**desc.index_dict, **kwargs}
kwargs.pop("sites")
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
if index == (None,):
index = ()
result = self.offset
for i in self.sites_to_sum:
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[(i, *index)]
) * self.desc._pyomo_var[(i, *index)]
return result
class SumBonds(Expr):
"""A class for expressions formed by summation over canvas bonds.
Attributes:
desc (Descriptor/Expr): descriptors or expressions to sum over
coefs (float/list<float>): coefficients to multiply contributions
from each bond
offset (float): coefficient to add to the expression
bonds_to_sum (list<int>): bonds to consider in the summation
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, desc, coefs=1.0, offset=0.0, bonds_to_sum=None, **kwargs):
"""Standard constructor for summation of bond contributions.
Args:
desc (Descriptor): descriptors or expressions to sum across all bonds
coefs (float/list<float>): Optional, coefficients to multiple each
bond term by. Default=1.0
offset (float): Optional, coefficient to add to the expression.
Default=0.0
bonds_to_sum (list<int>): Optional, subset of canvas bonds
(i.e., neighbor connections) to sum.
Default=None, meaning all bonds in the desc object are considered.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.desc = desc
self.coefs = coefs
self.offset = offset
self.bonds_to_sum = bonds_to_sum if bonds_to_sum is not None else desc.bonds
kwargs = {**desc.index_dict, **kwargs}
kwargs.pop("bonds")
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
if index == (None,):
index = ()
result = self.offset
for i, j in self.bonds_to_sum:
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[(i, j, *index)]
) * self.desc._pyomo_var[(i, j, *index)]
return result
class SumSiteTypes(Expr):
"""A class for expressions formed by summation over building block types.
Attributes:
desc (Descriptor/Expr): descriptors or expressions to sum over
coefs (float/list<float>): coefficients to multiply contributions
from each building block type
offset (float): coefficient to add to the expression
site_types_to_sum (list<BBlock>): building block types to consider in
the summation (index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, desc, coefs=1.0, offset=0.0, site_types_to_sum=None, **kwargs):
"""Standard constructor for summation of contributions by site-type.
Args:
desc (Descriptor): descriptors or expressions to sum across site types
coefs (float/list<float>): Optional, coefficients to multiple each
site-type term by. Default=1.0
offset (float): Optional, coefficient to add to the expression.
Default=0.0
bonds_types_to_sum (list<int>): Optional, subset of site types
to sum. Default=None, meaning all site-types in the desc object are
considered.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.desc = desc
self.coefs = coefs
self.offset = offset
self.site_types_to_sum = (
site_types_to_sum if site_types_to_sum is not None else desc.site_types
)
kwargs = {**desc.index_dict, **kwargs}
kwargs.pop("site_types")
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
assert index is not None
i, *index = index
if index == (None,):
index = ()
result = self.offset
for k in self.site_types_to_sum:
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[(i, k, *index)]
) * self.desc._pyomo_var[(i, k, *index)]
return result
class SumBondTypes(Expr):
"""A class for expressions formed by summation over building block types.
Attributes:
desc (Descriptor/Expr): descriptors or expressions to sum over
coefs (float/list<float>): coefficients to multiply contributions
from each pair of building block types
offset (float): coefficient to add to the expression
bond_types_to_sum (list<tuple<BBlock,BBlock>>): building block pairs
to consider in the summation
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, desc, coefs=1.0, offset=0.0, bond_types_to_sum=None, **kwargs):
"""Standard constructor for summation of contributions by bond-type.
Args:
desc (Descriptor): descriptors or expressions to sum across bond types
coefs (float/list<float>): Optional, coefficients to multiple each
bond-type term by. Default=1.0
offset (float): Optional, coefficient to add to the expression.
Default=0.0
bonds_types_to_sum (list<tuple<BBlock,BBlock>>): Optional, subset
of bond types to sum.
Default=None, meaning all bond-types in the desc object are
considered.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.desc = desc
self.coefs = coefs
self.offset = offset
self.bond_types_to_sum = (
bond_types_to_sum if bond_types_to_sum is not None else desc.bond_types
)
kwargs = {**desc.index_dict, **kwargs}
kwargs.pop("bond_types")
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
assert index is not None
i, j, *index = index
if index == (None,):
index = ()
result = self.offset
for k, l in self.bond_types_to_sum:
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[(i, j, k, l, *index)]
) * self.desc._pyomo_var[(i, j, k, l, *index)]
return result
class SumSitesAndTypes(Expr):
"""A class for expressions formed by summation over sites and building
block types.
Attributes:
desc (Descriptor/Expr): descriptors or expressions to sum over
coefs (float/list<float>): coefficients to multiply contributions
from each building block type
offset (float): coefficient to add to the expression
sites_to_sum (list<int>): sites to consider in the summation
site_types_to_sum (list<BBlock>): building block types to consider in
the summation (index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(
self,
desc,
coefs=1.0,
offset=0.0,
sites_to_sum=None,
site_types_to_sum=None,
**kwargs
):
"""Standard constructor for summation of site contributions.
Args:
desc (Descriptor): descriptors or expressions to sum across all
sites and site types
coefs (float/list<float>): Optional, coefficients to multiple each
site and site-type term by. Default=1.0
offset (float): Optional, coefficient to add to the expression.
Default=0.0
sites_to_sum (list<int>): Optional, subset of canvas sites to sum.
Default=None, meaning all sites in the desc object are considered.
site_types_to_sum (list<BBlock>): Optional, subset of site types to
sum. Default=None, meaning all site types in the desc object are
considered.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.desc = desc
self.coefs = coefs
self.offset = offset
self.sites_to_sum = sites_to_sum if sites_to_sum is not None else desc.sites
self.site_types_to_sum = (
site_types_to_sum if site_types_to_sum is not None else desc.site_types
)
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
if index == (None,):
index = ()
result = self.offset
for i in self.sites_to_sum:
for k in self.site_types_to_sum:
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[(i, k, *index)]
) * self.desc._pyomo_var[(i, k, *index)]
return result
class SumBondsAndTypes(Expr):
"""A class for expressions formed by summation over bonds and bond types.
Attributes:
desc (Descriptor/Expr): descriptors or expressions to sum over
coefs (float/list<float>): coefficients to multiply contributions
from each combination of bond and building block type
offset (float): coefficient to add to the expression
bonds_to_sum (list<tuple<int,int>>): bonds to consider in the summation
bond_types_to_sum (list<tuple<BBlock,BBlock>>): building block types to
consider in the summation
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(
self,
desc,
coefs=1.0,
offset=0.0,
bonds_to_sum=None,
bond_types_to_sum=None,
**kwargs
):
"""Standard constructor for summation of contributions by bond-type.
Args:
desc (Descriptor): descriptors or expressions to sum across bonds
and bond types
coefs (float/list<float>): Optional, coefficients to multiple each
term by. Default=1.0
offset (float): Optional, coefficient to add to the expression.
Default=0.0
bonds_to_sum (list<int>): Optional, subset of bonds to sum.
Default=None, meaning all bonds in the desc object are considered.
bonds_types_to_sum (list<int>): Optional, subset of bond types
to sum. Default=None, meaning all bond-types in the desc object are
considered.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.desc = desc
self.coefs = coefs
self.offset = offset
self.bonds_to_sum = bonds_to_sum if bonds_to_sum is not None else desc.bonds
self.bond_types_to_sum = (
bond_types_to_sum if bond_types_to_sum is not None else desc.bond_types
)
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
if index == (None,):
index = ()
result = self.offset
for i, j in self.bonds_to_sum:
for k, l in self.bond_types_to_sum:
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[i, j, k, l]
) * self.desc._pyomo_var[i, j, k, l]
return result
class SumConfs(Expr):
"""A class for expressions formed by summation over conformation types.
Attributes:
desc (Descriptor/Expr): descriptors or expressions to sum over
coefs (float/list<float>): coefficients to multiply contributions
from each building block type
offset (float): coefficient to add to the expression
confs_to_sum (list<int>): conformations to consider in the summation
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, Zic, coefs=1.0, offset=0.0, confs_to_sum=None, **kwargs):
"""Standard constructor for summation of bond contributions.
Args:
Zic (Descriptor): descriptors or expressions to sum across
conformations
coefs (float/list<float>): Optional, coefficients to multiple each
conformation term by. Default=1.0
offset (float): Optional, coefficient to add to the expression.
Default=0.0
confs_to_sum (list<int>): Optional, subset of conformations to sum
Default=None, meaning all conformations in the Zic object are
considered.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.Zic = Zic
self.coefs = coefs
self.offset = offset
self.confs_to_sum = confs_to_sum if confs_to_sum is not None else Zic.confs
kwargs = {**Zic.index_dict, **kwargs}
kwargs.pop("confs")
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
i, *index = index
if index == (None,):
index = ()
result = self.offset
for c in self.confs_to_sum:
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[(i, c, *index)]
) * self.Zic._pyomo_var[(i, c, *index)]
return result
class SumSitesAndConfs(Expr):
"""A class for expressions formed by summation over sites and building
block types.
Attributes:
desc (Descriptor/Expr): descriptors or expressions to sum over
coefs (float/list<float>): coefficients to multiply contributions
from each building block type
offset (float): coefficient to add to the expression
sites_to_sum (list<int>): sites to consider in the summation
confs_to_sum (list<BBlock>): conformations to consider in the summation
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(
self, Zic, coefs=1.0, offset=0.0, sites_to_sum=None, confs_to_sum=None, **kwargs
):
"""Standard constructor for summation of site and conformation
contributions.
Args:
Zic (Descriptor): descriptors or expressions to sum across all sites
and conformations.
coefs (float/list<float>): Optional, coefficients to multiple each
site and conformation term by. Default=1.0
offset (float): Optional, coefficient to add to the expression.
Default=0.0
sites_to_sum (list<int>): Optional, subset of canvas sites to sum.
Default=None, meaning all sites in the desc object are considered.
confs_to_sum (list<int>): Optional, subset of conformations to sum.
Default=None, meaning all conformations in the desc object are
considered.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.Zic = Zic
self.coefs = coefs
self.offset = offset
self.sites_to_sum = sites_to_sum if sites_to_sum is not None else Zic.sites
self.confs_to_sum = confs_to_sum if confs_to_sum is not None else Zic.confs
Expr.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_expr(self, index=None):
"""Interface for generating Pyomo expressions.
Args:
index (list): Optional, index to to create an instance of a Pyomo
expression. In the case of a scalar, the valid index is None.
Returns:
An instance of a Pyomo expression.
"""
if index == (None,):
index = ()
result = self.offset
for i in self.sites_to_sum:
for c in self.confs_to_sum:
result += (
self.coefs
if (
type(self.coefs) is float
or type(self.coefs) is int
or type(self.coefs) is SimpleParam
)
else self.coefs[(i, c, *index)]
) * self.Zic._pyomo_var[(i, c, *index)]
return result
class DescriptorRule(IndexedElem):
"""An abstract base class for rules to define material descriptors.
This class is only the abstract interface for other well-defined rules.
Rules get attached to MaterialDescriptor objects and are intended to be
interpretable in relation to the descriptor that they are attached to.
Examples:
'Coordination number is equal to the sum of variables for the presence
of any bond to the neighbors of a site.'
-> m.CNi.rules.append(EqualTo(SumNeighborBonds(m.Bondij)))
'Size of a cluster is equal to the number of of atoms'
-> m.ClusterSize.rules.append(EqualTo(SumSites(m.Yi)))
Rules can be applied over a subset of the design space (i.e., only for
some combinations of sites, site-types, etc.) by providing keywords
that are passed to the constructor of IndexedElem. Else, the relevant
indexes are inferred from the rule components.
Attributes:
(index information inherited from IndexedElem)
"""
def __init__(self, **kwargs):
"""Standard constructor for DescriptorRule base class.
Args:
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
IndexedElem.__init__(self, **kwargs)
@abstractmethod
def _pyomo_cons(self, var):
"""Abstract method to define necessary interface for descriptor rules.
Args:
var (MaterialDescriptor): Variable that the rule is attached to.
(i.e., the variable that should be read before the class name to
interpret the rule)
Returns:
(list<Constraint>) list of Pyomo constraint objects.
"""
raise NotImplementedError
class SimpleDescriptorRule(DescriptorRule):
"""An base class for simple rules with a left and right hand side.
This class is just intended to create a common interface for the
EqualTo, LessThan, and GreaterThan rules.
Attributes:
expr (Expr): Right-hand side expression for the rule.
(index information inherited from IndexedElem)
"""
# === STANDARD CONSTRUCTOR
def __init__(self, e, **kwargs):
"""Standard constructor for simple descriptor rules.
Args:
e (float/int/Expr): An expression to use are right hand side of
a rule. If a float or int, a LinearExpr is created for the user.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.expr = LinearExpr(offset=e) if type(e) is float or type(e) is int else e
kwargs = {**self.expr.index_dict, **kwargs}
DescriptorRule.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_cons(self, var):
"""Method to create a Pyomo constraint from this rule.
Args:
var (MaterialDescriptor): The descriptor to be defined by this rule.
Returns:
(list<Constraint>) list of Pyomo constraint objects.
"""
ConIndexes = IndexedElem.fromComb(var, self)
return [Constraint(*ConIndexes.index_sets, rule=self._pyomo_rule(var))]
def _pyomo_rule(self, LHS, operator, RHS):
"""Method to create a function for a Pyomo constraint rule.
Args:
LHS (MaterialDescriptor/Expr): The left hand side of a simple rule.
operator (function): The relationship to encode in a rule.
RHS (MaterialDescriptor/Expr): The right hand side of a simple rule.
Returns:
(function) A function interpretable by Pyomo for a 'rule' argument
"""
ConIndexes = IndexedElem.fromComb(LHS, RHS)
def rule(m, *args):
LHS_index = LHS.mask(args, ConIndexes)
RHS_index = RHS.mask(args, ConIndexes)
return operator(LHS._pyomo_expr(LHS_index), RHS._pyomo_expr(RHS_index))
return rule
class LessThan(SimpleDescriptorRule):
"""A class for rules implementing 'less than or equal to' an expression.
Spelled out: 'the descriptor is less than or equal to a linear expression'
Attributes:
expr (Expr): Right-hand side expression for the rule.
(index information inherited from IndexedElem)
See DescriptorRule for more information.
"""
# === STANDARD CONSTRUCTOR
# --- Inherited from SimpleDescriptorRule ---
# === PROPERTY EVALUATION METHODS
def _pyomo_rule(self, desc):
"""Method to create a function for a Pyomo constraint rule.
Args:
desc (MaterialDescriptor/Expr): A descriptor to define as 'less than'
the expression for this rule.
Returns:
(function) A function in the format of a Pyomo rule to construct a
constraint.
"""
def less_than(LHS, RHS):
return LHS <= RHS
return SimpleDescriptorRule._pyomo_rule(self, desc, less_than, self.expr)
class EqualTo(SimpleDescriptorRule):
"""A class for rules implementing 'equal to' an expression.
Spelled out: 'the descriptor is equal to a linear expression'
Attributes:
expr (Expr): Right-hand side expression for the rule.
(index information inherited from IndexedElem)
See DescriptorRule for more information.
"""
# === STANDARD CONSTRUCTOR
# --- Inherited from SimpleDescriptorRule ---
# === PROPERTY EVALUATION METHODS
def _pyomo_rule(self, desc):
"""Method to create a function for a Pyomo constraint rule.
Args:
desc (MaterialDescriptor/Expr): A descriptor to define as 'equal to'
the expression for this rule.
Returns:
(function) A function in the format of a Pyomo rule to construct a
constraint.
"""
def equal_to(LHS, RHS):
return LHS == RHS
return SimpleDescriptorRule._pyomo_rule(self, desc, equal_to, self.expr)
class GreaterThan(SimpleDescriptorRule):
"""A class for rules implementing 'greater than or equal to' an expr.
Spelled out: 'descriptor is greater than or equal to a linear expression'
Attributes:
expr (Expr): Right-hand side expression for the rule.
(index information inherited from IndexedElem)
See DescriptorRule for more information.
"""
# === STANDARD CONSTRUCTOR
# --- Inherited from SimpleDescriptorRule ---
# === PROPERTY EVALUATION METHODS
def _pyomo_rule(self, desc):
"""Method to create a function for a Pyomo constraint rule.
Args:
desc (MaterialDescriptor/Expr): A descriptor to define as 'greater
than' the expression for this rule.
Returns:
(function) A function in the format of a Pyomo rule to construct a
constraint.
"""
def greater_than(LHS, RHS):
return LHS >= RHS
return SimpleDescriptorRule._pyomo_rule(self, desc, greater_than, self.expr)
class FixedTo(DescriptorRule):
"""A class for rules that fix descriptors to required values.
Spelled out: 'the descriptor is fixed to a scalar value'
Attributes:
val (float): the value that the descriptor is fixed to.
(index information inherited from IndexedElem)
See DescriptorRule for more information.
"""
# === STANDARD CONSTRUCTOR
def __init__(self, val, **kwargs):
"""Standard constructor for FixedTo rules.
Args:
val (float): The value to fix descriptors to.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.val = val
DescriptorRule.__init__(self, **kwargs)
def _pyomo_cons(self, var):
"""Method to create a Pyomo constraint from this rule.
Args:
var (MaterialDescriptor): The descriptor to be defined by this rule.
Returns:
(list<Constraint>) list of Pyomo constraint objects.
"""
# NOTE: This method is used to ensure that basic variables that
# are fixed get referenced to write basic constraints in
# the model. Don't make constraints, but do instantiate
# variables
Comb = IndexedElem.fromComb(var, self)
for k in Comb.keys():
var._pyomo_var[k]
return []
class Disallow(DescriptorRule):
"""A class for rules that disallow a previously-identified design.
Spelled out: 'the descriptors must attain a different solution than
a given design'
Attributes:
D (Design): the design from which variable values to disallow are inferred
(index information inherited from IndexedElem)
See DescriptorRule for more information.
"""
# === STANDARD CONSTRUCTOR
def __init__(self, D, **kwargs):
"""Standard constructor for the Disallow rule.
Args:
D (Design): A design object to make infeasible in the resulting model
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.D = D
DescriptorRule.__init__(self, **kwargs)
def _pyomo_expr(self, var):
"""Method to create the integer cut for this disallowed design.
Args:
var (MaterialDescriptor): The descriptor to be defined by this rule.
Returns:
An instance of a Pyomo expression.
"""
if var.name == "Yi":
result = 0
for i in range(len(self.D.Canvas)):
if self.D.Contents[i] is None:
result += var._pyomo_var[i]
else:
result += 1 - var._pyomo_var[i]
elif var.name == "Yik":
result = 0
for i in range(len(self.D.Canvas)):
for k in self.D.NonVoidElems:
if self.D.Contents[i] is not k:
result += var._pyomo_var[i, k]
else:
result += 1 - var._pyomo_var[i, k]
else:
# NOTE: This rule was intended to disallow structures
# or labelings of structures (i.e., Yi or Yik
# variables). It is not clear how we can generally
# disallow any general combination of variables.
raise ValueError("Decide what to do in this case...")
return result
def _pyomo_cons(self, var):
"""
Method to create a Pyomo constraint from this rule.
Args:
var (MaterialDescriptor): The descriptor to be defined by this rule.
Returns:
(list<Constraint>) list of Pyomo constraint objects.
"""
return Constraint(expr=(self._pyomo_expr(var) >= 1))
class PiecewiseLinear(DescriptorRule):
"""A class for rules implementing 'equal to a piecewise linear function'.
Spelled out: 'the descriptor is equal to a piecewise linear expression'
Note: Innequalities of 'less than' or 'greater than' a piecewie function
can be achieved by introducing an auxiliary descriptor to be equal to the
piecewise function. Then, inequalities can be introduced using the
auxiliary descriptor. Alternatively, users can modify the con_type
attribute that is interpreted by Pyomo.
Attributes:
values (list<float>): values of univariate piecewise linear function at
each breakpoint.
breakpoints (list<float>): breakpoints of the piecewise linear function.
input_desc (MaterialDescriptor): descriptor as the argument to the
piecewise linear function
con_type (string): indicates the bound type of the piecewise function.
Options:
“UB” - relevant descriptor is bounded above by piecewise function.
“LB” - relevant descriptor is bounded below by piecewise function.
“EQ” - relevant descriptor is equal to the piecewise function. (Default)
See DescriptorRule for more information.
"""
# === STANDARD CONSTRUCTOR
def __init__(self, values, breakpoints, input_desc, con_type="EQ", **kwargs):
"""Standard constructor for simple descriptor rules.
Args:
values (list<float>): values of the function.
breakpoints (list<float>): breakpoints of the function.
input_desc (MaterialDescriptor): argument to the function
con_type (string): Optional, indicates the bound type of the
piecewise function
Options:
“UB” - bounded above by piecewise function.
“LB” - bounded below by piecewise function.
“EQ” - equal to the piecewise function. (Default)
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.values = values
self.breakpoints = breakpoints
self.input_desc = input_desc
self.con_type = con_type.upper()
DescriptorRule.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_cons(self, var):
"""Method to create a Pyomo constraint from this rule.
Args:
var (MaterialDescriptor): The descriptor to be defined by this rule.
Returns:
(list<Block>) list of Pyomo model block objects created by Piecewise
function.
"""
Comb = IndexedElem.fromComb(var, self)
return [
Piecewise(
*Comb.index_sets,
var._pyomo_var,
self.input_desc._pyomo_var,
pw_pts=self.breakpoints,
f_rule=self.values,
pw_constr_type=self.con_type,
pw_repn="MC"
)
]
class Implies(DescriptorRule):
"""A class for rules that define simple logical implications.
Spelled out: 'if this descriptor is true (i.e., equal to one),
then another set of simple rules also apply'
Attributes:
concs (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to enforce if the logical predicate is true.
(index information inherited from IndexedElem)
See DescriptorRule for more information.
"""
DEFAULT_BIG_M = 9999
# === STANDARD CONSTRUCTOR
def __init__(self, concs, **kwargs):
"""Standard constructor for Implies rule.
Args:
concs (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to conditionally enforce. Also, a single
conclusion can be provided (i.e., a tuple<MaterialDescriptor,
SimpleDescriptorRule>) and will be placed in a list.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.concs = concs if type(concs) is list else [concs]
Comb = IndexedElem.fromComb(
*(desc for desc, conc in self.concs), *(conc for desc, conc in self.concs)
)
kwargs = {**Comb.index_dict, **kwargs}
DescriptorRule.__init__(self, **kwargs)
def _pyomo_cons(self, var):
"""Method to create a Pyomo constraint from this rule.
Args:
var (MaterialDescriptor): The descriptor to be defined by this rule.
Returns:
(list<Constraint>) list of Pyomo constraint objects.
"""
result = []
for desc, conc in self.concs:
ConIndexes = IndexedElem.fromComb(var, desc, conc)
def rule_lb(m, *args):
v = var._pyomo_expr(index=var.mask(args, ConIndexes))
d = desc._pyomo_expr(index=desc.mask(args, ConIndexes))
c = conc.expr._pyomo_expr(index=conc.expr.mask(args, ConIndexes))
body_lb = getLB(d - c)
MLB = body_lb if body_lb is not None else -Implies.DEFAULT_BIG_M
return MLB * (1 - v) <= d - c
def rule_ub(m, *args):
v = var._pyomo_expr(index=var.mask(args, ConIndexes))
d = desc._pyomo_expr(index=desc.mask(args, ConIndexes))
c = conc.expr._pyomo_expr(index=conc.expr.mask(args, ConIndexes))
body_ub = getUB(d - c)
MUB = body_ub if body_ub is not None else Implies.DEFAULT_BIG_M
return d - c <= MUB * (1 - v)
if isinstance(conc, LessThan) or isinstance(conc, EqualTo):
result.append(Constraint(*ConIndexes.index_sets, rule=rule_ub))
if isinstance(conc, GreaterThan) or isinstance(conc, EqualTo):
result.append(Constraint(*ConIndexes.index_sets, rule=rule_lb))
return result
class NegImplies(DescriptorRule):
"""A class for rules that define logical implications with negation.
Spelled out: 'if this descriptor is not true (i.e., is equal to zero),
then another simple rule also applies'
Attributes:
concs (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to enforce if the logical predicate is false.
(index information inherited from IndexedElem)
See DescriptorRule for more information.
"""
DEFAULT_BIG_M = 9999
# === STANDARD CONSTRUCTOR
def __init__(self, concs, **kwargs):
"""Standard constructor for NegImplies rule.
Args:
concs (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to conditionally enforce. Also, a single
conclusion can be provided (i.e., a tuple<MaterialDescriptor,
SimpleDescriptorRule>) and will be placed in a list.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.concs = concs if type(concs) is list else [concs]
Comb = IndexedElem.fromComb(
*(desc for desc, conc in self.concs), *(conc for desc, conc in self.concs)
)
kwargs = {**Comb.index_dict, **kwargs}
DescriptorRule.__init__(self, **kwargs)
def _pyomo_cons(self, var):
"""Method to create a Pyomo constraint from this rule.
Args:
var (MaterialDescriptor): The descriptor to be defined by this rule.
Returns:
(list<Constraint>) list of Pyomo constraint objects.
"""
result = []
for desc, conc in self.concs:
ConIndexes = IndexedElem.fromComb(var, desc, conc)
def rule_lb(m, *args):
v = var._pyomo_expr(index=var.mask(args, ConIndexes))
d = desc._pyomo_expr(index=desc.mask(args, ConIndexes))
c = conc.expr._pyomo_expr(index=conc.expr.mask(args, ConIndexes))
body_lb = getLB(d - c)
MLB = body_lb if body_lb is not None else -NegImplies.DEFAULT_BIG_M
return MLB * (v) <= d - c
def rule_ub(m, *args):
v = var._pyomo_expr(index=var.mask(args, ConIndexes))
d = desc._pyomo_expr(index=desc.mask(args, ConIndexes))
c = conc.expr._pyomo_expr(index=conc.expr.mask(args, ConIndexes))
body_ub = getUB(d - c)
MUB = body_ub if body_ub is not None else NegImplies.DEFAULT_BIG_M
return d - c <= MUB * v
if isinstance(conc, LessThan) or isinstance(conc, EqualTo):
result.append(Constraint(*ConIndexes.index_sets, rule=rule_ub))
if isinstance(conc, GreaterThan) or isinstance(conc, EqualTo):
result.append(Constraint(*ConIndexes.index_sets, rule=rule_lb))
return result
class ImpliesSiteCombination(DescriptorRule):
"""A class for rules that define logical implications between two sites.
Spelled out: 'if this bond-indexed descriptor is true (i.e., is equal
to one), then a pair of simple rules hold on the two bonding sites'
Attributes:
canv (Canvas): the data structure to identify neighbor connections
to apply rules over.
concis (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to enforce at the first site in the pair if
the logical predicate is true.
concjs (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to enforce at the second site in the pair if
the logical predicate is true.
symmetric_bonds (bool): flag to indicate if implications should be
applied over symmetric bond indices
(index information inherited from IndexedElem)
See DescriptorRule for more information.
"""
DEFAULT_BIG_M = 9999
# === STANDARD CONSTRUCTOR
def __init__(self, canv, concis, concjs, symmetric_bonds=False, **kwargs):
"""Standard constructor for ImpliesSiteCombination rules.
Args:
canv (Canvas): the data structure to identify neighbor connections
to apply rules over.
concis (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to conditionally enforce at the first
site in a bond.
Note: single conclusions can be provided (i.e., a
tuple<MaterialDescriptor,SimpleDescriptorRule>) and will
be placed in lists.
concjs (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to conditionally enforce at the second
site in a bond.
Note: single conclusions can be provided (i.e., a
tuple<MaterialDescriptor,SimpleDescriptorRule>) and will
be placed in lists.
symmetric_bonds (bool): flag to indicate if a symmetric versions
of bonds should be enumerated or if both directions should
be included.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.canv = canv
self.concis = concis if type(concis) is list else [concis]
self.concjs = concjs if type(concjs) is list else [concjs]
self.symmetric_bonds = symmetric_bonds
Combi = IndexedElem.fromComb(
*(desc for desc, conc in self.concis), *(conc for desc, conc in self.concis)
)
Combj = IndexedElem.fromComb(
*(desc for desc, conc in self.concjs), *(conc for desc, conc in self.concjs)
)
assert Combi.sites is not None and Combj.sites is not None
if "bonds" not in kwargs:
kwargs["bonds"] = [
(i, j)
for i in Combi.sites
for j in canv.NeighborhoodIndexes[i]
if (
j is not None
and j in Combj.sites
and (not symmetric_bonds or j > i)
)
]
if sum(Combi.dims) > 1:
raise NotImplementedError(
"Additional indexes are not supported, please contact MatOpt developer for "
"possible feature addition"
)
DescriptorRule.__init__(self, **kwargs)
def _pyomo_cons(self, var):
"""Method to create a Pyomo constraint from this rule.
Args:
var (MaterialDescriptor): The descriptor to be defined by this rule.
Returns:
(list<Constraint>) list of Pyomo constraint objects.
"""
assert var.bonds is not None
Comb = IndexedElem.fromComb(var, self)
result = []
# NOTE: After much confusion, I found a bug in the line of code
# below. Be careful not to use variable names "expr"
# because it gets mixed up with the Pyomo module "expr".
# No error, but it gives garbage expressions and wasn't
# clear to me what was being generated...
# NOTE: Not clear if this was caused by module "expr" or the
# conflict of two local "expr,conc" objects in the two
# for loops...
# for expr,conc in self.concis:
for expri, conci in self.concis:
def rule_i_lb(m, i, j):
e = expri._pyomo_expr(index=(i,))
c = conci.expr._pyomo_expr(index=(i,))
body = e - c
body_LB = getLB(body)
MLBi = (
body_LB
if body_LB is not None
else -ImpliesSiteCombination.DEFAULT_BIG_M
)
return MLBi * (1 - var._pyomo_var[i, j]) <= body
def rule_i_ub(m, i, j):
e = expri._pyomo_expr(index=(i,))
c = conci.expr._pyomo_expr(index=(i,))
body = e - c
body_UB = getUB(body)
MUBi = (
body_UB
if body_UB is not None
else ImpliesSiteCombination.DEFAULT_BIG_M
)
return body <= MUBi * (1 - var._pyomo_var[i, j])
if isinstance(conci, GreaterThan) or isinstance(conci, EqualTo):
result.append(Constraint(*Comb.index_sets, rule=rule_i_lb))
if isinstance(conci, LessThan) or isinstance(conci, EqualTo):
result.append(Constraint(*Comb.index_sets, rule=rule_i_ub))
# NOTE: See note above for variable name "expr"
# for expr,conc in self.concjs:
for exprj, concj in self.concjs:
def rule_j_lb(m, i, j):
e = exprj._pyomo_expr(index=(j,))
c = concj.expr._pyomo_expr(index=(j,))
body = e - c
body_LB = getLB(body)
MLBj = (
body_LB
if body_LB is not None
else -ImpliesSiteCombination.DEFAULT_BIG_M
)
return MLBj * (1 - var._pyomo_var[i, j]) <= body
def rule_j_ub(m, i, j):
e = exprj._pyomo_expr(index=(j,))
c = concj.expr._pyomo_expr(index=(j,))
body = e - c
body_UB = getUB(body)
MUBj = (
body_UB
if body_UB is not None
else ImpliesSiteCombination.DEFAULT_BIG_M
)
return body <= MUBj * (1 - var._pyomo_var[i, j])
if isinstance(concj, GreaterThan) or isinstance(concj, EqualTo):
result.append(Constraint(*Comb.index_sets, rule=rule_j_lb))
if isinstance(concj, LessThan) or isinstance(concj, EqualTo):
result.append(Constraint(*Comb.index_sets, rule=rule_j_ub))
return result
class ImpliesNeighbors(DescriptorRule):
"""A class for rules that define logical implications on neighbor sites.
Spelled out: 'if this site-indexed descriptor is true (i.e., is equal
to one), then a set of simple rules hold on each of the neighboring
sites'
Attributes:
concs (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to enforce if the logical predicate is true.
neighborhoods (list<list<int>>): neighborhood data structure to use
if you do not want to use the neighborhoods of the descriptor
that this rule is attached to.
(index information inherited from IndexedElem)
See DescriptorRule for more information on rules and Canvas for more
information on 'neighborhoods'.
"""
DEFAULT_BIG_M = 9999
# === STANDARD CONSTRUCTOR
def __init__(self, concs, neighborhoods=None, **kwargs):
"""Standard constructor for ImpliesNeighbors rules.
Args:
concs (list<tuple<MaterialDescriptor,SimpleDescriptorRule>>):
list of conclusions to conditionally enforce. Also, a single
conclusion can be provided (i.e., a tuple<MaterialDescriptor,
SimpleDescriptorRule>) and will be placed in a list.
neighborhoods (list<list<int>>) Optional, data structure to use
as neighborhoods of interest. If not provided, then the
neighborhoods of the descriptor that this rule is attached to
is used.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self.concs = concs if type(concs) is list else [concs]
self.neighborhoods = neighborhoods
Comb = IndexedElem.fromComb(
*(desc for desc, conc in self.concs), *(conc for desc, conc in self.concs)
)
assert Comb.sites is not None
kwargs = {**Comb.index_dict, **kwargs}
DescriptorRule.__init__(self, **kwargs)
# === PROPERTY EVALUATION METHODS
def _pyomo_cons(self, var):
"""Method to create a Pyomo constraint from this rule.
Args:
var (MaterialDescriptor): The descriptor to be defined by this rule.
Returns:
(list<Constraint>) list of Pyomo constraint objects.
"""
var_dict_wo_s = var.index_dict
var_dict_wo_s.pop("sites") # no need to capture these sites
neighborhoods = (
self.neighborhoods
if self.neighborhoods is not None
else var.canv.NeighborhoodIndexes
)
bonds = [(i, j) for i in var.sites for j in neighborhoods[i] if j is not None]
result = []
# NOTE: After much confusion, I found a bug in the line of code
# below. Be careful not to use variable names "expr"
# because it gets mixed up with the Pyomo module "expr".
# No error, but it gives garbage expressions and wasn't
# clear to me what was being generated...
# for expr,conc in self.concs:
for expr_, conc in self.concs:
Comb = IndexedElem.fromComb(expr_, conc)
r_dict_wo_s = Comb.index_dict
r_dict_wo_s.pop("sites") # no need to capture these sites
ConIndexes = IndexedElem.fromComb(
IndexedElem(bonds=bonds),
IndexedElem(**var_dict_wo_s),
IndexedElem(**r_dict_wo_s),
)
def rule_lb(m, *args):
i, j, *args = args
v = var._pyomo_var[var.mask((i, None, *args), ConIndexes)]
e = expr_._pyomo_expr(index=expr_.mask((j, None, *args), ConIndexes))
c = conc.expr._pyomo_expr(
index=conc.expr.mask((j, None, *args), ConIndexes)
)
body = e - c
body_LB = getLB(body)
MLB = (
body_LB if body_LB is not None else -ImpliesNeighbors.DEFAULT_BIG_M
)
return MLB * (1 - v) <= body
def rule_ub(m, *args):
i, j, *args = args
v = var._pyomo_var[var.mask((i, None, *args), ConIndexes)]
e = expr_._pyomo_expr(index=expr_.mask((j, None, *args), ConIndexes))
c = conc.expr._pyomo_expr(
index=conc.expr.mask((j, None, *args), ConIndexes)
)
body = e - c
body_UB = getUB(body)
MUB = body_UB if body_UB is not None else ImpliesNeighbors.DEFAULT_BIG_M
return body <= MUB * (1 - v)
if isinstance(conc, GreaterThan) or isinstance(conc, EqualTo):
result.append(Constraint(*ConIndexes.index_sets, rule=rule_lb))
if isinstance(conc, LessThan) or isinstance(conc, EqualTo):
result.append(Constraint(*ConIndexes.index_sets, rule=rule_ub))
return result
[docs]
class MaterialDescriptor(IndexedElem):
"""A class to represent material geometric and energetic descriptors.
This class holds the information to define mathematical optimization
variables for the properties of materials. Additionally, each descriptor
has a 'rules' list to which the user can append rules defining the
descriptor and constraining the design space.
Attributes:
name (string): A unique (otherwise Pyomo will complain) name
canv (``Canvas``): The canvas that the descriptor will be indexed over
atoms (list<``BBlock``>): The building blocks to index the descriptor over.
confDs (list<``Design``>): The designs for conformations to index over.
integer (bool): Flag to indicate if the descriptor takes integer values.
binary (bool): Flag to indicate if the descriptor takes boolean values.
rules (list<``DescriptorRules``>): List of rules to define and constrain
the material descriptor design space.
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index.
See ``IndexedElem`` for more information on indexing.
See ``DescriptorRule`` for information on defining descriptors.
"""
DBL_TOL = 1e-5
# === STANDARD CONSTRUCTOR
def __init__(
self,
name,
canv=None,
atoms=None,
confDs=None,
bounds=(None, None),
integer=False,
binary=False,
rules=None,
**kwargs
):
"""Standard constructor for material descriptors.
Note: It is generally not necessary for users to create
MaterialDescriptors themselves. Instead, use the
MatOptModel.add____Descriptor() methods for the right
type of descriptor (i.e., Site, Bond, etc.).
Args:
name (string): A unique (otherwise Pyomo will complain) name
canv (Canvas): The canvas that the descriptor will be indexed over
atoms (list<BBlock>): Building blocks to index the descriptor over.
confDs (list<Design>): The designs for conformations to index over.
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
**kwargs: Optional, index information passed to IndexedElem if
interested in a subset of indices.
Possible choices: sites, bonds, site_types, bond_types, confs.
"""
self._name = name
self._canv = canv
self._atoms = atoms
self._confDs = confDs
self._integer = integer or binary
self._binary = binary
if rules is None:
rules = []
self._rules = rules if type(rules) is list else [rules]
self._bounds = bounds
self._pyomo_var = None # Will be set by MatOptModel._make_pyomo_model
IndexedElem.__init__(self, **kwargs)
# === AUXILIARY METHODS
def _fix_pyomo_var_by_rule(self, r, m):
if self.name in ("Yik", "Yi", "Xijkl", "Xij", "Cikl", "Ci", "Zic"):
return self.__fix_basic_pyomo_vars_by_rule(r, m)
else:
Comb = IndexedElem.fromComb(self, r)
for k in Comb.keys():
self._pyomo_var[k].fix(r.val)
def __fix_basic_pyomo_vars_by_rule(self, r, m):
Comb = IndexedElem.fromComb(self, r)
if self.name == "Yik":
for i in Comb.sites:
for k in Comb.site_types:
fixYik(m, i, k, r.val)
elif self.name == "Yi":
for i in Comb.sites:
fixYi(m, i, r.val)
elif self.name == "Xijkl":
for i, j in Comb.bonds:
for k, l in Comb.bond_types:
fixXijkl(m, i, j, k, l, r.val)
elif self.name == "Xij":
for i, j in Comb.bonds:
fixXij(m, i, j, r.val)
elif self.name == "Cikl":
for i in Comb.sites:
for k, l in Comb.bond_types:
fixCikl(m, i, k, l, r.val)
elif self.name == "Ci":
for i in Comb.sites:
fixCi(m, i, r.val)
elif self.name == "Zic":
for i in Comb.sites:
for c in Comb.confs:
fixZic(m, i, c, r.val)
# === PROPERTY EVALUATION METHODS
def _pyomo_cons(self, m):
"""Create a list of Pyomo constraints related to this descriptor."""
result = []
for rule in self.rules:
if rule is not None:
result.extend(rule._pyomo_cons(self))
return result
@property
def _pyomo_bounds(self):
"""Creates a bound rule/tuple that can interpreted by Pyomo."""
if type(self.bounds) is tuple:
return self.bounds
elif type(self.bounds) is dict:
def rule_gen(m, *args):
if args is not None and len(args) == 1:
args = args[0]
return self.bounds[args]
return rule_gen
else:
# Else, assume that the user knows what they're doing
# with functions for pyomo bounds
return self.bounds
def _pyomo_expr(self, index=None):
"""Interprets a variable as a Pyomo expression.
Note: This is just necessary so that we can conveniently interpret
MaterialDescriptor objects in place of Expr objects.
"""
return self._pyomo_var[index]
@property
def values(self):
"""Creates a dictionary of descriptor values after optimization.
Note: Uses the Pyomo 'value' function and only works after the
optimization of a model.
Returns:
(dict) Dictionary of keys to values after optimization.
"""
return {index: value(self._pyomo_var[index]) for index in self.keys()}
# === BASIC QUERY METHODS
@property
def name(self):
return self._name
@property
def canv(self):
return self._canv
@property
def atoms(self):
return self._atoms
@property
def confDs(self):
return self._confDs
@property
def bounds(self):
return self._bounds
@property
def integer(self):
return self._integer
@property
def binary(self):
return self._binary
@property
def continuous(self):
return not self.integer
@property
def rules(self):
return self._rules
[docs]
class MatOptModel(object):
"""A class for the specification of a materials optimization problem.
Once all the material information is specified, we use this class to
specify the material design problem of interest. This class is intended
to be interpretable without mathematical optimization background while
the conversion to Pyomo optimization models happens automatically.
Attributes:
canv (``Canvas``): The canvas of the material design space
atoms (list<``BBlock``>): The list of building blocks to consider.
Note: This list does not need to include a void-atom type. We use
'None' to represent the absence of any building block at a given site.
confDs (list<``Design``>): The list of conformations to consider.
"""
# === STANDARD CONSTRUCTOR
def __init__(self, canv, atoms=None, confDs=None):
"""Standard constructor for materials optimization problems.
Args:
canv (``Canvas``): The canvas of the material design space
atoms (list<``BBlock``>): The list of building blocks to consider.
Note: This list does not need to include a void-atom type. We use
'None' to represent the absence of any building block at a given site.
confDs (list<``Design``>): The list of conformations to consider.
"""
self._canv = canv
self._atoms = atoms
self._confDs = confDs
self._descriptors = []
self.addSitesDescriptor("Yi", binary=True, rules=None)
self.addBondsDescriptor("Xij", binary=True, rules=None)
self.addNeighborsDescriptor("Ci", integer=True, rules=None)
self.addSitesTypesDescriptor("Yik", binary=True, rules=None)
self.addBondsTypesDescriptor("Xijkl", binary=True, rules=None)
self.addNeighborsTypesDescriptor("Cikl", integer=True, rules=None)
self.addSitesConfsDescriptor("Zic", binary=True, rules=None)
# === MANIPULATION METHODS
def addGlobalDescriptor(
self, name, bounds=(None, None), integer=False, binary=False, rules=None
):
"""
Method to add scalar descriptor to the model.
Args:
name (string): A unique (otherwise Pyomo will complain) name.
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
"""
assert not hasattr(self, name)
Desc = MaterialDescriptor(
name=name, bounds=bounds, integer=integer, binary=binary, rules=rules
)
setattr(self, name, Desc)
self._descriptors.append(Desc)
def addSitesDescriptor(
self,
name,
sites=None,
bounds=(None, None),
integer=False,
binary=False,
rules=None,
):
"""Method to add a site-indexed descriptor to the model.
Args:
name (string): A unique (otherwise Pyomo will complain) name.
sites (list<int>): Optional, subset of canvas sites to index the new
descriptor over.
Default: None (i.e., all sites in canvas are considered)
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
"""
assert not hasattr(self, name)
sites = sites if sites is not None else list(range(len(self.canv)))
Desc = MaterialDescriptor(
name=name,
canv=self.canv,
sites=sites,
bounds=bounds,
integer=integer,
binary=binary,
rules=rules,
)
setattr(self, name, Desc)
self._descriptors.append(Desc)
def addBondsDescriptor(
self,
name,
bonds=None,
bounds=(None, None),
integer=False,
binary=False,
rules=None,
symmetric_bonds=False,
):
"""Method to add a bond-indexed descriptor to the model.
Args:
name (string): A unique (otherwise Pyomo will complain) name.
bonds (list<tuple<int,int>>): Optional, subset of canvas neighbor
pairs to index the new descriptor over.
Default: None (i.e., all neighbor pairs included)
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
"""
assert not hasattr(self, name)
bonds = (
bonds
if bonds is not None
else [
(i, j)
for i in range(len(self.canv))
for j in self.canv.NeighborhoodIndexes[i]
if (j is not None and (not symmetric_bonds or j > i))
]
)
Desc = MaterialDescriptor(
name=name,
canv=self.canv,
bonds=bonds,
bounds=bounds,
integer=integer,
binary=binary,
rules=rules,
)
setattr(self, name, Desc)
self._descriptors.append(Desc)
def addNeighborsDescriptor(
self,
name,
sites=None,
bounds=(None, None),
integer=False,
binary=False,
rules=None,
):
"""Method to add a neighborhood-indexed descriptor to the model.
Args:
name (string): A unique (otherwise Pyomo will complain) name.
sites (list<int>): Optional, subset of canvas sites to index the new
descriptor over.
Default: None (i.e., all sites in canvas are considered)
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
"""
assert not hasattr(self, name)
sites = sites if sites is not None else list(range(len(self.canv)))
Desc = MaterialDescriptor(
name=name,
canv=self.canv,
sites=sites,
bounds=bounds,
integer=integer,
binary=binary,
rules=rules,
)
setattr(self, name, Desc)
self._descriptors.append(Desc)
def addGlobalTypesDescriptor(
self,
name,
site_types=None,
bond_types=None,
bounds=(None, None),
integer=False,
binary=False,
rules=None,
):
"""Method to add a type-indexed descriptor to the model.
Args:
name (string): A unique (otherwise Pyomo will complain) name.
site_types (list<BBlock>): Optional, subset of building block types
to index the new descriptor over.
Note: If both site_types and bond_types are left to None, then
we decide to index over building block types by default.
bond_types (list<tuple<BBlock,BBlock>>): Optional, subset of building
block pairs to index the new descriptor over.
Note: If both site_types and bond_types are left to None, then
we decide to index over building block types by default.
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
"""
assert not hasattr(self, name)
site_types = (
site_types
if site_types is not None
else (self.atoms if bond_types is None else None)
)
bond_types = bond_types
Desc = MaterialDescriptor(
name=name,
atoms=self.atoms,
site_types=site_types,
bond_types=bond_types,
bounds=bounds,
integer=integer,
binary=binary,
rules=rules,
)
setattr(self, name, Desc)
self._descriptors.append(Desc)
def addSitesTypesDescriptor(
self,
name,
sites=None,
site_types=None,
bounds=(None, None),
integer=False,
binary=False,
rules=None,
):
"""Method to add a site-and-type-indexed descriptor to the model.
Args:
name (string): A unique (otherwise Pyomo will complain) name.
sites (list<int>): Optional, subset of canvas sites to index the new
descriptor over.
Default: None (i.e., all sites in canvas are considered)
site_types (list<BBlock>): Optional, subset of building block types
to index the new descriptor over.
Default: None (i.e., all building block types are considered)
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
"""
assert not hasattr(self, name)
sites = sites if sites is not None else list(range(len(self.canv)))
site_types = site_types if site_types is not None else self.atoms
Desc = MaterialDescriptor(
name=name,
atoms=self.atoms,
canv=self.canv,
sites=sites,
site_types=site_types,
bounds=bounds,
integer=integer,
binary=binary,
rules=rules,
)
setattr(self, name, Desc)
self._descriptors.append(Desc)
def addBondsTypesDescriptor(
self,
name,
bonds=None,
bond_types=None,
bounds=(None, None),
integer=False,
binary=False,
rules=None,
symmetric_bonds=False,
):
"""Method to add a bond-and-type-indexed descriptor to the model.
Args:
name (string): A unique (otherwise Pyomo will complain) name.
bonds (list<tuple<int,int>>): Optional, subset of canvas neighbor
pairs to index the new descriptor over.
Default: None (i.e., all neighbor pairs included)
bond_types (list<tuple<BBlock,BBlock>>): Optional, subset of
building block pairs to index the new descriptor over.
Default: None (i.e., all pairs of building blocks considered)
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
"""
assert not hasattr(self, name)
bonds = (
bonds
if bonds is not None
else [
(i, j)
for i in range(len(self.canv))
for j in self.canv.NeighborhoodIndexes[i]
if (j is not None and (not symmetric_bonds or j > i))
]
)
bond_types = (
bond_types
if bond_types is not None
else [(k, l) for k in self.atoms for l in self.atoms]
)
Desc = MaterialDescriptor(
name=name,
atoms=self.atoms,
canv=self.canv,
bonds=bonds,
bond_types=bond_types,
bounds=bounds,
integer=integer,
binary=binary,
rules=rules,
)
setattr(self, name, Desc)
self._descriptors.append(Desc)
def addNeighborsTypesDescriptor(
self,
name,
sites=None,
bond_types=None,
bounds=(None, None),
integer=False,
binary=False,
rules=None,
):
"""Method to add a neighborhood-bond-type-indexed descriptor.
Args:
name (string): A unique (otherwise Pyomo will complain) name.
sites (list<int>): Optional, subset of canvas sites to index the new
descriptor over.
Default: None (i.e., all sites in canvas are considered)
bond_types (list<tuple<BBlock,BBlock>>): Optional, subset of building
block pairs to index the new descriptor over.
Default: None (i.e., all pairs of building blocks considered)
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
"""
assert not hasattr(self, name)
sites = sites if sites is not None else list(range(len(self.canv)))
bond_types = (
bond_types
if bond_types is not None
else [(k, l) for k in self.atoms for l in self.atoms]
)
Desc = MaterialDescriptor(
name=name,
atoms=self.atoms,
canv=self.canv,
sites=sites,
bond_types=bond_types,
bounds=bounds,
integer=integer,
binary=binary,
rules=rules,
)
setattr(self, name, Desc)
self._descriptors.append(Desc)
def addSitesConfsDescriptor(
self,
name,
sites=None,
confs=None,
bounds=(0, 1),
integer=True,
binary=True,
rules=None,
):
"""Method to add a site-and-conformation-indexed descriptor.
Args:
name (string): A unique (otherwise Pyomo will complain) name.
sites (list<int>): Optional, subset of canvas sites to index the new
descriptor over.
Default: None (i.e., all sites in canvas are considered)
confs (list<int>): Optional, subset of conformation indices to index
the new descriptor over.
Default: None (i.e., all conformations included)
bounds (tuple/dict/func): If tuple, the lower and upper bounds on the
descriptor values across all indices. If dict, the bounds can be
individually set for each index. Otherwise, advanced users can
specify a function to be interpreted by Pyomo.
integer (bool): Flag to indicate if the descriptor is integer.
binary (bool): Flag to indicate if the descriptor is boolean.
rules (list<DescriptorRules>): List of rules to define and constrain
the material descriptor design space.
"""
sites = sites if sites is not None else list(range(len(self.canv)))
confs = (
confs
if confs is not None
else (list(range(len(self.confDs))) if self.confDs is not None else None)
)
Desc = MaterialDescriptor(
name=name,
canv=self.canv,
confDs=self.confDs,
sites=sites,
confs=confs,
bounds=bounds,
integer=integer,
binary=binary,
rules=rules,
)
setattr(self, name, Desc)
self._descriptors.append(Desc)
# === PROPERTY EVALUATION METHODS
[docs]
def maximize(self, func, **kwargs):
"""Method to maximize a target functionality of the material model.
Args:
func (``MaterialDescriptor``/``Expr``): Material functionality to optimize.
**kwargs: Arguments to ``MatOptModel.optimize``
Returns:
(``Design``/list<``Design``>) Optimal designs.
Raises:
``pyomo.common.errors.ApplicationError`` if MatOpt can not find
usable solver (CPLEX or NEOS-CPLEX)
See ``MatOptModel.optimize`` method for details.
"""
return self.optimize(func, sense=maximize, **kwargs)
[docs]
def minimize(self, func, **kwargs):
"""Method to minimize a target functionality of the material model.
Args:
func (``MaterialDescriptor``/``Expr``): Material functionality to optimize.
**kwargs: Arguments to ``MatOptModel.optimize``
Returns:
(``Design``/list<``Design``>) Optimal designs.
Raises:
``pyomo.common.errors.ApplicationError`` if MatOpt can not find usable
solver (CPLEX or NEOS-CPLEX)
See ``MatOptModel.optimize`` method for details.
"""
return self.optimize(func, sense=minimize, **kwargs)
[docs]
def optimize(
self,
func,
sense,
nSolns=1,
tee=True,
disp=1,
keepfiles=False,
tilim=3600,
trelim=None,
solver="cplex",
):
"""Method to create and optimize the materials design problem.
This method automatically creates a new optimization model every
time it is called. Then, it solves the model via Pyomo with the
CPLEX solver.
If multiple solutions (called a 'solution pool') are desired, then
the nSolns argument can be provided and the populate method will
be called instead.
Args:
func (``MaterialDescriptor``/``Expr``): Material functionality to optimize.
sense (int): flag to indicate the choice to minimize or maximize the
functionality of interest.
Choices: minimize/maximize (Pyomo constants 1,-1 respectively)
nSolns (int): Optional, number of Design objects to return.
Default: 1 (See ``MatOptModel.populate`` for more information)
tee (bool): Optional, flag to turn on solver output.
Default: True
disp (int): Optional, flag to control level of MatOpt output.
Choices: 0: No MatOpt output (other than solver tee) 1: MatOpt
output for outer level method 2: MatOpt output for solution pool &
individual solns. Default: 1
keepfiles (bool): Optional, flag to save temporary pyomo files.
Default: True
tilim (float): Optional, solver time limit (in seconds).
Default: 3600
trelim (float): Optional, solver tree memory limit (in MB).
Default: None (i.e., Pyomo/CPLEX default)
solver (str): Solver choice. Currently only cplex or neos-cplex are supported
Default: cplex
Returns:
(``Design``/list<``Design``>) Optimal design or designs, depending
on the number of solutions requested by argument ``nSolns``.
Raises:
``pyomo.common.errors.ApplicationError`` if MatOpt can not find
usable solver (CPLEX or NEOS-CPLEX)
"""
if nSolns > 1:
return self.populate(
func,
sense=sense,
nSolns=nSolns,
tee=tee,
disp=disp,
keepfiles=keepfiles,
tilim=tilim,
trelim=trelim,
solver=solver,
)
elif nSolns == 1:
self._pyomo_m = self._make_pyomo_model(func, sense)
return self.__solve_pyomo_model(tee, disp, keepfiles, tilim, trelim, solver)
[docs]
def populate(
self,
func,
sense,
nSolns,
tee=True,
disp=1,
keepfiles=False,
tilim=3600,
trelim=None,
solver="cplex",
):
"""Method to a pool of solutions that optimize the material model.
This method automatically creates a new optimization model every
time it is called. Then, it solves the model via Pyomo with the
CPLEX solver.
The populate method iteratively solves the model, interprets the
solution as a Design object, creates a constraint to disallow that
design, and resolves to find the next best design. We build a pool
of Designs that are guaranteed to be the nSolns-best solutions in the
material design space.
Args:
func (``MaterialDescriptor``/``Expr``): Material functionality to optimize.
sense (int): flag to indicate the choice to minimize or maximize
the functionality of interest.
Choices: minimize/maximize (Pyomo constants 1,-1 respectively)
nSolns (int): Optional, number of Design objects to return.
Default: 1 (See ``MatOptModel.populate`` for more information)
tee (bool): Optional, flag to turn on solver output.
Default: True
disp (int): Optional, flag to control level of MatOpt output.
Choices: 0: No MatOpt output (other than solver tee) 1: MatOpt
output for outer level method 2: MatOpt output for solution
pool & individual solns. Default: 1
keepfiles (bool): Optional, flag to save temporary pyomo files.
Default: True
tilim (float): Optional, solver time limit (in seconds).
Default: 3600
trelim (float): Optional, solver tree memory limit (in MB).
Default: None (i.e., Pyomo/CPLEX default)
solver (str): Solver choice. Currently only cplex or neos-cplex are
supported Default: cplex
Returns:
(list<``Design``>) A list of optimal Designs in order of decreasing
optimality.
Raises:
``pyomo.common.errors.ApplicationError`` if MatOpt can not find
usable solver (CPLEX or NEOS-CPLEX)
"""
self._pyomo_m = self._make_pyomo_model(func, sense)
self._pyomo_m.iSolns = Set(initialize=list(range(nSolns)))
self._pyomo_m.IntCuts = Constraint(self._pyomo_m.iSolns)
def dispPrint(*args):
if disp > 0:
print(*args)
else:
pass
Ds = []
for iSoln in range(nSolns):
dispPrint("Starting populate for solution #{}... ".format(iSoln))
D = self.__solve_pyomo_model(
tee, disp - 1, keepfiles, tilim, trelim, solver
)
if D is not None:
dispPrint(
"Found solution with objective: {}".format(value(self._pyomo_m.obj))
)
Ds.append(D)
if len(self._pyomo_m.Yik) > 0:
self._pyomo_m.IntCuts.add(
index=iSoln, expr=(Disallow(D)._pyomo_expr(self.Yik) >= 1)
)
elif len(self._pyomo_m.Yi) > 0:
self._pyomo_m.IntCuts.add(
index=iSoln, expr=(Disallow(D)._pyomo_expr(self.Yi) >= 1)
)
else:
raise NotImplementedError("Decide what to do " "in this case...")
else:
dispPrint("No solution found. Terminating populate.")
break
dispPrint("Identified {} solutions via populate.".format(len(Ds)))
return Ds
def _make_pyomo_model(self, obj_expr, sense):
"""Method to create a Pyomo concrete model object.
This method creates a Pyomo model and also modifies several objects
in the MatOpt framework. It creates Pyomo variable objects and
attaches references to those variables on each of the
MaterialDescriptors attached to the MatOptModel.
Args:
obj_expr (MaterialDescriptor/Expr): Material functionality to
optimize.
sense (int): flag to indicate the choice to minimize or maximize the
functionality of interest.
Choices: minimize/maximize (Pyomo constants 1,-1 respectively)
Returns:
(ConcreteModel) Pyomo model object.
"""
m = makeMyPyomoBaseModel(self.canv, Atoms=self.atoms, Confs=self.confDs)
self.Yi._pyomo_var = m.Yi
self.Xij._pyomo_var = m.Xij
self.Ci._pyomo_var = m.Ci
self.Yik._pyomo_var = m.Yik
self.Xijkl._pyomo_var = m.Xijkl
self.Cikl._pyomo_var = m.Cikl
self.Zic._pyomo_var = m.Zic
for desc in self._descriptors:
if desc.name not in ("Yik", "Yi", "Xijkl", "Xij", "Cikl", "Ci", "Zic"):
v = Var(
*desc.index_sets,
domain=(
Binary if desc.binary else (Integers if desc.integer else Reals)
),
bounds=desc._pyomo_bounds,
dense=False
)
setattr(m, desc.name, v)
setattr(desc, "_pyomo_var", v)
for desc in self._descriptors:
for c, pyomo_con in enumerate(desc._pyomo_cons(m)):
setattr(m, "Assign{}_{}".format(desc.name, c), pyomo_con)
if sum(obj_expr.dims) == 0:
m.obj = Objective(expr=obj_expr._pyomo_expr(index=(None,)), sense=sense)
else:
raise TypeError(
"The MaterialDescriptor chosen is not supported to be an objective, please contact MatOpt "
"developer for potential fix"
)
# NOTE: The timing of the call to addConsForGeneralVars is important
# We need to call it after all user-defined descriptors are
# encoded.
# Else, lots of constraints for basic variables that are not
# necessary will be written.
addConsForGeneralVars(m)
for desc in self._descriptors:
for r in desc.rules:
if isinstance(r, FixedTo):
desc._fix_pyomo_var_by_rule(r, m)
return m
def __solve_pyomo_model(self, tee, disp, keepfiles, tilim, trelim, solver):
"""Method to solve the formulated Pyomo optimization model.
This function is intended to standardize the printout and
approach for converting results into Designs that is used
by the optimize and populate methods.
Args:
tee (bool): Flag to turn on solver output.
disp (int): Flag to control level of MatOpt output.
keepfiles (bool): Flag to save temporary pyomo files.
tilim (float): Solver time limit (in seconds).
trelim (float): Solver tree memory limit (in MB).
solver (str): Solver choice. Currently only cplex or
neos-cplex are supported
Returns:
(Design) The best design identified by the solver, if any.
The quality of the solution (optimal vs best found at
termination) can be found by reading the output display.
In the case that the model was infeasible or no solution
could be identified, the method returns 'None'.
"""
if solver == "cplex":
opt = SolverFactory("cplex")
opt.options["mip_tolerances_absmipgap"] = 0.0
opt.options["mip_tolerances_mipgap"] = 0.0
if tilim is not None:
opt.options["timelimit"] = tilim
if trelim is not None:
opt.options["mip_limits_treememory"] = trelim
res = opt.solve(
self._pyomo_m, tee=tee, symbolic_solver_labels=True, keepfiles=keepfiles
)
elif solver == "neos-cplex":
with SolverManagerFactory("neos") as manager:
opt = SolverFactory("cplex")
opt.options["absmipgap"] = 0.0 # NOTE: different option names
opt.options["mipgap"] = 0.0
if tilim is not None:
opt.options["timelimit"] = tilim
if trelim is not None:
opt.options["treememory"] = trelim
res = manager.solve(self._pyomo_m, opt=opt)
else:
raise NotImplementedError(
"MatOpt is tailored to perform best with CPLEX (locally or through NEOS), "
"please contact MatOpt developer for additional solver support "
)
solver_status = res.solver.status
solver_term = res.solver.termination_condition
soln_status = res.solution.status
has_solution = (
soln_status == SolutionStatus.optimal
or soln_status == SolutionStatus.feasible
or soln_status == SolutionStatus.bestSoFar
or soln_status == SolutionStatus.globallyOptimal
or soln_status == SolutionStatus.locallyOptimal
)
# NOTE: The block below is a hack to get around the fact that Pyomo
# solution statuses are not flagged correctly all the time. If
# solution status was unknown (but actually optimal or feasible)
# then this should (hopefully) flag the solution as available
if soln_status == SolutionStatus.unknown:
value(self._pyomo_m.obj)
has_solution = True
def dispPrint(*args):
if disp > 0:
print(*args)
else:
pass
if solver_status == SolverStatus.ok:
dispPrint("The solver exited normally.")
if (
solver_term == TerminationCondition.optimal
or solver_term == TerminationCondition.locallyOptimal
or solver_term == TerminationCondition.globallyOptimal
):
# NOTE: This assertion should be re-enabled when Pyomo bug
# described above is fixed.
# assert(soln_status==SolutionStatus.optimal)
dispPrint("A feasible and provably optimal solution " "is available.")
else:
dispPrint(
"The solver exited due to termination criteria: {}".format(
solver_term
)
)
if has_solution:
dispPrint(
"A feasible (but not provably optimal) "
"solution is available."
)
else:
dispPrint("No solution available.")
else:
dispPrint(
"The solver did not exit normally. Status: {}".format(solver_status)
)
if has_solution:
dispPrint("The Design has objective: {}".format(value(self._pyomo_m.obj)))
result = Design(self.canv)
setDesignFromModel(result, self._pyomo_m)
return result
else:
return None
# === BASIC QUERY METHODS
@property
def canv(self):
return self._canv
@property
def atoms(self):
return self._atoms
@property
def confDs(self):
return self._confDs
@property
def descriptors(self):
return self._descriptors