import copy
from typing import Optional, TypeVar, Union
import numpy as np
import libcasm.casmglobal
import libcasm.clexulator as casmclex
import libcasm.configuration as casmconfig
import libcasm.counter
from libcasm.configuration._misc import (
equivalent_order_parameters_index,
make_canonical_order_parameters,
)
from libcasm.irreps import (
SubWedge,
)
array_like = TypeVar("array_like")
def _is_corner_point(
x: np.ndarray,
shape_factor: np.ndarray,
abs_tol: float = libcasm.casmglobal.TOL,
) -> bool:
if x.transpose() @ shape_factor @ x > 1.0 + abs_tol:
return True
return False
def _subwedge_counter(
subwedge: SubWedge,
stop: float,
num: int,
) -> tuple[list[np.ndarray], libcasm.counter.IntCounter]:
"""Generate a counter for integer coordinates of subwedge points"""
dim = subwedge.trans_mat.shape[1]
start = [0.0] * dim
stop = [stop] * dim
num = [num] * dim
# For axes with multiplicity==1, include both positive and negative coordinates;
# otherwise, only include positive
subwedge_axis_index = 0
for irrep_wedge in subwedge.irrep_wedges:
for i, m in enumerate(irrep_wedge.mult):
if m == 1:
start[subwedge_axis_index] = -stop[subwedge_axis_index]
# keep point at 0.0 * axis
num[subwedge_axis_index] = 2 * num[subwedge_axis_index] - 1
subwedge_axis_index += 1
xi = [
np.linspace(_start, _stop, num=_num)
for _start, _stop, _num in zip(start, stop, num)
]
counter = libcasm.counter.IntCounter(
initial=[0] * dim,
final=[len(x) - 1 for x in xi],
increment=[1] * dim,
)
return (xi, counter)
[docs]
def meshgrid_points(
background: casmconfig.Configuration,
dof_space: casmclex.DoFSpace,
xi: list[array_like],
skip_equivalents: bool = False,
abs_tol: float = libcasm.casmglobal.TOL,
):
"""Generate points in a meshgrid
Parameters
----------
background: casmconfig.Configuration
The background configuration on which enumeration takes place.
dof_space: casmclex.DoFSpace
Specifies the DoF being enumerated and the basis of the DoFSpace are the
axes on which the meshgrid is constructed and enumeration takes place.
For local DoF, the dof_space supercell must tile the background supercell.
Not supported for ``dof_space.dof_key == "occ"``, which raises.
xi : list[array_like]
1-D arrays, `[x1, x2, ...]`, representing the coordinates of a grid
generated as if by `np.meshgrid`. x1 gives the coordinates along the first
`dof_space` basis vector, x2 along the second `dof_space` vector, etc.
There must be one array per `dof_space` basis vector.
skip_equivalents: bool = False
If True, skip symmetrically equivalent points, using the symmetry of
the `background` configuration.
abs_tol: float = :data:`~libcasm.casmglobal.TOL`
The absolute tolerance used for checking equivalence.
Yields
------
eta: np.ndarray
A point in the meshgrid.
"""
if skip_equivalents:
fg = casmconfig.make_invariant_subgroup(background)
dof_space_rep = casmconfig.make_dof_space_rep(group=fg, dof_space=dof_space)
canonical_eta_list = []
dim = len(xi)
counter = libcasm.counter.IntCounter(
initial=[0] * dim,
final=[len(x) - 1 for x in xi],
increment=[1] * dim,
)
for indices in counter:
eta = np.array([x[i] for x, i in zip(xi, indices)])
if skip_equivalents:
if (
equivalent_order_parameters_index(
canonical_eta_list=canonical_eta_list,
eta=eta,
dof_space_rep=dof_space_rep,
abs_tol=abs_tol,
)
is not None
):
continue
else:
canonical_eta_list.append(
make_canonical_order_parameters(
eta=eta, dof_space_rep=dof_space_rep, abs_tol=abs_tol
)
)
yield eta
[docs]
def irreducible_wedge_points(
background: casmconfig.Configuration,
dof_space: casmclex.DoFSpace,
irreducible_wedge: list[SubWedge],
stop: float,
num: int,
trim_corners: bool = True,
skip_equivalents: bool = False,
abs_tol: float = libcasm.casmglobal.TOL,
):
"""Generate points in the irreducible wedge
Parameters
----------
background: casmconfig.Configuration
The background configuration on which enumeration takes place.
dof_space: casmclex.DoFSpace
Specifies the DoF being enumerated and the basis of the DoFSpace are the
axes on which the meshgrid is constructed and enumeration takes place.
For local DoF, the dof_space supercell must tile the background supercell.
Not supported for ``dof_space.dof_key == "occ"``, which raises.
irreducible_wedge: list[SubWedge]
The irreducible wedge, from a :class:`VectorSpaceSymReport` calculated
using :func:`casmconfig.dof_space_analysis` of `background`.
stop: float
The ending value of the sequence of grid points along each wedge edge
vector. The same value is used along each edge vector. Must be positive.
The start value is 0.0, unless the corresponding vector has a symmetric
multiplicity of 1, in which case the start value is ``-stop``. This treats
cases such as :math:`e_1` strain for which positive and negative values are
symmetrically distinct.
num: int
Number of grid points to generate along each wedge edge vector, using
`numpy.linspace`. The origin and `stop` value are included in the sequence.
Must be positive.
If the corresponding vector has symmetric multiplicity of 1, then
``num*2-1`` is used along that vector to include positive and negative
coordinates while keeping the same spacing.
trim_corners: bool = True
If True, skip grid points that lie outside the ellipsoid inscribed
within the extrema of the grid.
skip_equivalents: bool = False
If True, skip symmetrically equivalent points, using the symmetry of
the `background` configuration.
abs_tol: float = :data:`~libcasm.casmglobal.TOL`
The absolute tolerance used for checking equivalence.
Yields
------
(subwedge_index, eta): Tuple[int, np.ndarray]
subwedge_index: int
The index of the current SubWedge in the irreducible wedge.
eta: np.ndarray
A point in the irreducible wedge, as coordinates with respect to
`dof_space.basis`.
"""
if skip_equivalents:
fg = casmconfig.make_invariant_subgroup(background)
dof_space_rep = casmconfig.make_dof_space_rep(group=fg, dof_space=dof_space)
canonical_eta_list = []
if trim_corners:
dim = dof_space.subspace_dim
shape_factor = np.eye(dim)
for i in range(dim):
shape_factor[i, i] = 1.0 / (abs(stop) ** 2)
for subwedge_index, subwedge in enumerate(irreducible_wedge):
xi, counter = _subwedge_counter(subwedge, stop, num)
for indices in counter:
eta_subwedge = np.array([x[i] for x, i in zip(xi, indices)])
if trim_corners:
if _is_corner_point(
x=eta_subwedge, shape_factor=shape_factor, abs_tol=abs_tol
):
continue
eta = dof_space.basis_inv @ subwedge.trans_mat @ eta_subwedge
if skip_equivalents:
if (
equivalent_order_parameters_index(
canonical_eta_list=canonical_eta_list,
eta=eta,
dof_space_rep=dof_space_rep,
abs_tol=abs_tol,
)
is not None
):
continue
else:
canonical_eta_list.append(
make_canonical_order_parameters(
eta=eta, dof_space_rep=dof_space_rep, abs_tol=abs_tol
)
)
yield (subwedge_index, eta)
[docs]
class ConfigEnumMeshGrid:
"""Enumerate configuration with continuous DoF values in a mesh grid"""
def __init__(
self,
prim: casmconfig.Prim,
supercell_set: Optional[casmconfig.SupercellSet] = None,
):
"""
.. rubric:: Constructor
Parameters
----------
prim: casmconfig.Prim
The Prim
supercell_set: Optional[casmconfig.SupercellSet] = None
If not None, generated :class:`~casmconfig.Supercell` are constructed by
adding in the :class:`~casmconfig.SupercellSet`.
"""
self._prim = prim
self._supercell_set = supercell_set
# Set and updated during an enumeration
self._background = None
self._dof_space = None
self._enum_index = None
self._order_parameters = None
self._subwedge_index = None
@property
def prim(self) -> casmconfig.Prim:
"""The Prim"""
return self._prim
@property
def supercell_set(self) -> Optional[casmconfig.SupercellSet]:
"""If not None, generated :class:`~casmconfig.Supercell` are constructed by
adding in the :class:`~casmconfig.SupercellSet`."""
return self._supercell_set
@property
def background(self) -> Optional[casmconfig.Configuration]:
"""During enumeration, `background` is set to the current background
configuration in which enumeration is performed."""
return self._background
@property
def dof_space(self) -> Optional[casmclex.DoFSpace]:
"""During enumeration, `dof_space` is set to the current DoFspace in which
enumeration is performed."""
return self._dof_space
@property
def enum_index(self) -> Optional[int]:
"""During enumeration, `enum_index` is incremented to count over the
combinations of background configuration and dof space on which enumeration is
performed, starting from 0."""
return self._enum_index
@property
def order_parameters(self) -> Optional[np.ndarray]:
"""During enumeration, `order_parameters` is set to the current order parameter
values use to construct the generated configuration."""
return self._order_parameters
@property
def subwedge_index(self) -> Optional[int]:
"""During enumeration by irreducible wedge, `subwedge_index` is set to the
index of the current SubWedge of the irreducible wedge."""
return self._subwedge_index
def _by_grid_coordinates(
self,
background: casmconfig.Configuration,
dof_space: casmclex.DoFSpace,
xi: list[array_like],
skip_equivalents: bool = False,
abs_tol: float = libcasm.casmglobal.TOL,
):
"""Enumerate on a meshgrid, specified with coordinates along each axis of a \
DoFSpace basis
Parameters
----------
background: casmconfig.Configuration
The background configuration on which enumeration takes place.
dof_space: casmclex.DoFSpace
Specifies the DoF being enumerated and the basis of the DoFSpace are the
axes on which the meshgrid is constructed and enumeration takes place.
For local DoF, the dof_space supercell must tile the background supercell.
Not supported for ``dof_space.dof_key == "occ"``, which raises.
xi : list[array_like]
1-D arrays, `[x1, x2, ...]`, representing the coordinates of a grid
generated as if by `np.meshgrid`. x1 gives the coordinates along the first
`dof_space` basis vector, x2 along the second `dof_space` vector, etc.
There must be one array per `dof_space` basis vector.
skip_equivalents: bool = False
If True, skip symmetrically equivalent configurations.
abs_tol: float = :data:`~libcasm.casmglobal.TOL`
The absolute tolerance used for checking equivalence.
Yields
------
config: casmconfig.Configuration
A :class:`~casmconfig.Configuration` on a point in the DoF values meshgrid.
"""
self._background = background
self._dof_space = dof_space
if self._enum_index is None:
self._enum_index = 0
else:
self._enum_index += 1
if self.supercell_set is not None:
self.supercell_set.add_supercell(self.background.supercell)
if dof_space.dof_key == "occ":
raise Exception(
"Error in ConfigEnumMeshGrid._by_grid_coordinates: "
'Invalid dof_space, dof_space.dof_key == "occ" is not supported'
)
# Note: Do skip_equivalents work here, using
# casmconfig.make_canonical_configuration, instead of generating points without
# equivalents using meshgrid_points(..., skip_equivalents=True) because:
# 1) This is exact: does not depend on background choice
# 2) This seems to be faster: meshgrid_points currently uses comparisons of the
# order parameter vectors in Python, vs Configuration comparisons in C++
if skip_equivalents:
is_canonical_background_supercell = casmconfig.is_canonical_supercell(
background.supercell
)
if is_canonical_background_supercell:
canonical_configs = casmconfig.ConfigurationSet()
else:
canonical_configs = []
for eta in meshgrid_points(
background=background,
dof_space=dof_space,
xi=xi,
skip_equivalents=False, # see note above
abs_tol=abs_tol,
):
config = copy.copy(self.background)
config.set_order_parameters(
dof_space=dof_space,
order_parameters=eta,
)
if skip_equivalents:
canonical_config = casmconfig.make_canonical_configuration(config)
if canonical_config in canonical_configs:
continue
else:
if is_canonical_background_supercell:
canonical_configs.add(canonical_config)
else:
canonical_configs.append(canonical_config)
self._order_parameters = eta
yield config
[docs]
def by_grid_coordinates(
self,
background: casmconfig.Configuration,
dof_space: casmclex.DoFSpace,
xi: list[array_like],
skip_equivalents: bool = False,
abs_tol: float = libcasm.casmglobal.TOL,
):
"""Enumerate on a meshgrid, specified with coordinates along each axis of a \
DoFSpace basis
Parameters
----------
background: casmconfig.Configuration
The background configuration on which enumeration takes place.
dof_space: casmclex.DoFSpace
Specifies the DoF being enumerated and the basis of the DoFSpace are the
axes on which the meshgrid is constructed and enumeration takes place.
For local DoF, the dof_space supercell must tile the background supercell.
Not supported for ``dof_space.dof_key == "occ"``, which raises.
xi : list[array_like]
1-D arrays, `[x1, x2, ...]`, representing the coordinates of a grid
generated as if by `np.meshgrid`. x1 gives the coordinates along the first
`dof_space` basis vector, x2 along the second `dof_space` vector, etc.
There must be one array per `dof_space` basis vector.
skip_equivalents: bool = False
If True, skip symmetrically equivalent configurations.
abs_tol: float = :data:`~libcasm.casmglobal.TOL`
The absolute tolerance used for checking equivalence.
Yields
------
config: casmconfig.Configuration
A :class:`~casmconfig.Configuration` on a point in the DoF values meshgrid.
"""
# these will be set by self._by_grid_coordinates
self._background = None
self._dof_space = None
self._enum_index = None
if dof_space.dof_key == "occ":
raise Exception(
"Error in ConfigEnumMeshGrid.by_grid_coordinates: "
'Invalid dof_space, dof_space.dof_key == "occ" is not supported'
)
for config in self._by_grid_coordinates(
background=background,
dof_space=dof_space,
xi=xi,
skip_equivalents=skip_equivalents,
abs_tol=abs_tol,
):
yield config
[docs]
def by_range(
self,
background: casmconfig.Configuration,
dof_space: casmclex.DoFSpace,
start: Union[float, list[float]],
stop: Union[float, list[float]],
num: Union[int, list[int], None] = None,
step: Union[float, list[float], None] = None,
skip_equivalents: bool = False,
abs_tol: float = libcasm.casmglobal.TOL,
):
"""Enumerate on a meshgrid, with coordinates specified by a range along each \
axis of a DoFSpace basis
Parameters
----------
background: casmconfig.Configuration
The background configuration on which enumeration takes place.
dof_space: casmclex.DoFSpace
Specifies the DoF being enumerated and the basis of the DoFSpace are the
axes on which the meshgrid is constructed and enumeration takes place.
For local DoF, the dof_space supercell must tile the background supercell.
Not supported for ``dof_space.dof_key == "occ"``, which raises.
start: Union[float, list[float]]
The starting value of the sequence of grid points along each `dof_space`
basis vector. If scalar-valued, the same value is used along each
`dof_space` basis vector.
stop: Union[float, list[float]]
The ending value of the sequence of grid points along each `dof_space`
basis vector. If scalar-valued, the same value is used along each
`dof_space` basis vector.
num: Union[int, list[int], None] = None
Number of grid points to generate along each `dof_space`
basis vector, using `numpy.linspace`. If scalar-valued, the same value is
used along each `dof_space` basis vector. The `stop` value is included in
the sequence. Must be non-negative. One and only one of `num` or `step`
must be provided.
step: Union[float, list[float], None]=None
Spacing between grid points along each `dof_space` basis vector, in a
sequence generated using `numpy.arange`. If scalar-valued, the same value is
used along each `dof_space` basis vector. The sequence does not include the
`stop` value, except in some cases where `step` is not an integer and
floating point round-off affects the length of the sequence. Must be
non-negative. One and only one of `num` or `step` must be provided.
skip_equivalents: bool = False
If True, skip symmetrically equivalent points.
abs_tol: float = :data:`~libcasm.casmglobal.TOL`
The absolute tolerance used for checking equivalence.
Yields
------
config: casmconfig.Configuration
A :class:`~casmconfig.Configuration` on a point in the DoF values meshgrid.
"""
# these will be set by self._by_grid_coordinates
self._background = None
self._dof_space = None
self._enum_index = None
if dof_space.dof_key == "occ":
raise Exception(
"Error in ConfigEnumMeshGrid.by_range: "
'Invalid dof_space, dof_space.dof_key == "occ" is not supported'
)
dim = dof_space.subspace_dim
if isinstance(start, float):
start = [start] * dim
if isinstance(stop, float):
stop = [stop] * dim
if step is not None and num is None:
if isinstance(step, float):
step = [step] * dim
xi = [
np.arange(_start, _stop, _step)
for _start, _stop, _step in zip(start, stop, step)
]
elif num is not None and step is None:
if isinstance(num, int):
num = [num] * dim
xi = [
np.linspace(_start, _stop, num=_num)
for _start, _stop, _num in zip(start, stop, num)
]
else:
raise Exception(
"Error in ConfigEnumMeshGrid.by_range: "
"One and only one of 'step' or 'num' is required"
)
for config in self._by_grid_coordinates(
background=background,
dof_space=dof_space,
xi=xi,
skip_equivalents=skip_equivalents,
abs_tol=abs_tol,
):
yield config
[docs]
def by_irreducible_wedge(
self,
background: casmconfig.Configuration,
dof_space: casmclex.DoFSpace,
irreducible_wedge: list[SubWedge],
stop: float,
num: int,
trim_corners: bool = True,
skip_equivalents: bool = False,
abs_tol: float = libcasm.casmglobal.TOL,
):
"""Enumerate on a meshgrid in each subwedge of the irreducible wedge of a \
DoFSpace
Parameters
----------
background: casmconfig.Configuration
The background configuration on which enumeration takes place.
dof_space: casmclex.DoFSpace
Specifies the DoF being enumerated and the basis of the DoFSpace are the
axes on which the meshgrid is constructed and enumeration takes place.
For local DoF, the dof_space supercell must tile the background supercell.
Not supported for ``dof_space.dof_key == "occ"``, which raises.
irreducible_wedge: list[SubWedge]
The irreducible wedge, from a :class:`VectorSpaceSymReport` calculated
using :func:`casmconfig.dof_space_analysis` of `background`.
stop: float
The ending value of the sequence of grid points along each wedge edge
vector. The same value is used along each edge vector. Must be positive.
The start value is 0.0, unless the corresponding vector has a symmetric
multiplicity of 1, in which case the start value is ``-stop``. This treats
cases such as :math:`e_1` strain for which positive and negative values are
symmetrically distinct.
num: int
Number of grid points to generate along each wedge edge vector, using
`numpy.linspace`. The origin and `stop` value are included in the sequence.
Must be positive.
If the corresponding vector has symmetric multiplicity of 1, then
``num*2-1`` is used along that vector to include positive and negative
coordinates while keeping the same spacing.
skip_equivalents: bool = False
If True, skip symmetrically equivalent configurations.
trim_corners: bool = True
If True, skip grid points that lie outside the ellipsoid inscribed
within the extrema of the grid.
Yields
------
config: casmconfig.Configuration
A :class:`~casmconfig.Configuration` on a point in the irreducible wedge.
"""
self._background = background
self._dof_space = dof_space
self._enum_index = 0
if self.supercell_set is not None:
self.supercell_set.add_supercell(self.background.supercell)
if dof_space.dof_key == "occ":
raise Exception(
"Error in ConfigEnumMeshGrid.grid_coordinates: "
'Invalid dof_space, dof_space.dof_key == "occ" is not supported'
)
# Note: Do skip_equivalents work here, using
# casmconfig.make_canonical_configuration, instead of generating points without
# equivalents using meshgrid_points(..., skip_equivalents=True) because:
# 1) This is exact: does not depend on background choice
# 2) This seems to be faster: meshgrid_points currently uses comparisons of the
# order parameter vectors in Python, vs Configuration comparisons in C++
if skip_equivalents:
is_canonical_background_supercell = casmconfig.is_canonical_supercell(
background.supercell
)
if is_canonical_background_supercell:
canonical_configs = casmconfig.ConfigurationSet()
else:
canonical_configs = []
for subwedge_index, eta in irreducible_wedge_points(
background=background,
dof_space=dof_space,
irreducible_wedge=irreducible_wedge,
stop=stop,
num=num,
trim_corners=trim_corners,
skip_equivalents=False, # see note above
abs_tol=abs_tol,
):
config = copy.copy(self.background)
config.set_order_parameters(
dof_space=dof_space,
order_parameters=eta,
)
if skip_equivalents:
canonical_config = casmconfig.make_canonical_configuration(config)
if canonical_config in canonical_configs:
continue
else:
if is_canonical_background_supercell:
canonical_configs.add(canonical_config)
else:
canonical_configs.append(canonical_config)
self._subwedge_index = subwedge_index
self._order_parameters = eta
yield config