#################################################################################
# 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.
#################################################################################
import numpy as np
from copy import deepcopy
from ..util.util import myArrayEq, myPointsEq, ListHasPoint
from .parsers.PDB import readPointsAndAtomsFromPDB
from .parsers.XYZ import readPointsAndAtomsFromXYZ
from .parsers.CFG import readPointsAndAtomsFromCFG
from .geometry import RectPrism
[docs]
class Canvas(object):
"""A class for combining geometric points and neighbors.
This class contains a list of Cartesian points coupled with a graph of nodes for sites and arcs
for bonds. A ``Canvas`` object establishes a mapping from the abstract, mathematical modeling of
materials as graphs to the geometry of the material lattice. The list of points and neighbor
connections necessary to create a ``Canvas`` object can be obtained from the combination of
``Lattice``, ``Shape``, and ``Tiling`` objects.
"""
DBL_TOL = 1e-5
# === STANDARD CONSTRUCTOR
def __init__(self, Points=None, NeighborhoodIndexes=None, DefaultNN=0):
if Points is None and NeighborhoodIndexes is None:
Points = []
NeighborhoodIndexes = []
elif Points is None:
Points = [None] * len(NeighborhoodIndexes)
elif NeighborhoodIndexes is None:
NeighborhoodIndexes = [[None] * DefaultNN for _ in range(len(Points))]
self._Points = Points
self._NeighborhoodIndexes = NeighborhoodIndexes
self.__DefaultNN = DefaultNN
assert self.isConsistentWithDesign()
# === CONSTRUCTOR - From PDB File
@classmethod
def fromPDB(cls, filename, Lat=None, DefaultNN=0):
"""Make Canvas by reading from PDB file.
Args:
filename(str): Name of PDB file to read.
Lat (Lattice, optional): A lattice to define neighbor connections
DefaultNN(int, optional): Number of neighbors to allocate
in neighborhood. (Default value = 0)
Returns:
Canvas: A new Canvas object.
"""
Pts, _ = readPointsAndAtomsFromPDB(filename)
result = cls(Points=Pts, DefaultNN=DefaultNN)
if Lat is not None:
result.setNeighborsFromFunc(Lat.getNeighbors)
return result
@classmethod
def fromXYZ(cls, filename, Lat=None, DefaultNN=0):
"""Make Canvas by reading from XYZ file.
Args:
filename(str): Name of XYZ file to read.
Lat (Lattice, optional): A lattice to define neighbor connections
DefaultNN(int, optional): Number of neighbors to allocate
in neighborhood. (Default value = 0)
Returns:
Canvas: A new Canvas object.
"""
Pts, _ = readPointsAndAtomsFromXYZ(filename)
result = cls(Points=Pts, DefaultNN=DefaultNN)
if Lat is not None:
result.setNeighborsFromFunc(Lat.getNeighbors)
return result
@classmethod
def fromCFG(cls, filename, Lat=None, DefaultNN=0):
"""Make Canvas by reading from CFG file.
Args:
filename(str): Name of CFG file to read.
Lat (Lattice, optional): A lattice to define neighbor connections
DefaultNN(int, optional): Number of neighbors to allocate
in neighborhood. (Default value = 0)
Returns:
Canvas: A new Canvas object.
"""
Pts, _ = readPointsAndAtomsFromCFG(filename)
result = cls(Points=Pts, DefaultNN=DefaultNN)
if Lat is not None:
result.setNeighborsFromFunc(Lat.getNeighbors)
return result
# === CONSTRUCTOR - From Lattice and Shape
@classmethod
def fromLatticeAndShape(
cls, Lat, S, Seed=np.array([0, 0, 0], dtype=float), DefaultNN=0
):
"""Make Canvas by iterating over Lattice points that fit in Shape.
This constructor starts from a seed location and repeatedly adds
neighbors until there are no more neighbors that lie inside the
provided Shape object. It is potentially very slow and should only
be considered if other methods are unavailable.
Prefer Canvas.fromLatticeAndShapeScan.
Args:
DefaultNN:
Lat(Lattice): Lattice to provide getNeighbors function.
S(Shape): Shape to iterate over.
Seed(numpy.ndarray, optional): Location to begin adding neighbors from.
Should be on the Lattice. (Default value =
np.array([0,0,0],dtype=float))
DefaultNN(int, optional): Number of neighbors to allocate
in neighborhood. (Default value = 0)
Returns:
Canvas: A new Canvas object.
"""
result = cls(DefaultNN=DefaultNN)
Stack = [Seed]
while len(Stack) > 0:
P = Stack.pop()
if not result.hasPoint(P) and P in S:
PNs = Lat.getNeighbors(P)
result.addLocation(P, len(PNs))
# NOTE: At first, we checked if P needed to be added
# (i.e., if it was not already in the Stack)
# but doing so was significantly slower than
# just extending the Stack without checks and
# just checking if P was already in the result
Stack.extend(PNs)
result.setNeighborsFromFunc(Lat.getNeighbors)
return result
@classmethod
def fromLatticeAndShapeScan(cls, Lat, argPolyhedron, DefaultNN=0):
"""Make Canvas by iterating over Lattice points that fit in Shape.
This constructor takes advantage of methods in Lattice to
efficiently scan over sites. This requires the Lattice.Scan
method to produce a generator object.
Args:
DefaultNN:
Lat(Lattice): Lattice with has defined Scan method.
argPolyhedron(Polyhedron: Polyhedron): Shape to iterate over.
NOTE: A Polyhedron is required because there is a
complicated step in finding bounds for the Shape
in the reference lattice space that is not
generally valid for all Shapes.
DefaultNN(int, optional): Number of neighbors to allocate
in neighborhood. (Default value = 0)
Returns:
Canvas: A new Canvas object.
"""
result = cls(DefaultNN=DefaultNN)
BBox = RectPrism.fromPointsBBox(argPolyhedron.getBounds())
for P in Lat.Scan(BBox):
if P in argPolyhedron:
result.addLocation(P, len(Lat.getNeighbors(P)))
result.setNeighborsFromFunc(Lat.getNeighbors)
return result
@classmethod
def fromLatticeAndTiling(
cls, Lat, T, Seed=np.array([0, 0, 0], dtype=float), DefaultNN=0
):
"""Make Canvas by iterating over Lattice points that fit in Tiling.
See documentation for fromLatticeAndShape.
This constructor additionally makes the resulting Canvas periodic.
Args:
DefaultNN:
Lat(Lattice): Lattice to provide getNeighbors function.
T(Tiling): Tiling which provides a Shape to iterate over.
Seed(numpy.ndarray, optional): Location to begin adding neighbors from.
Should be on the Lattice. (Default value = np.array([0,0,0],dtype=float))
DefaultNN(int, optional): Number of neighbors to allocate
in neighborhood. (Default value = 0)
Returns:
Canvas: A new Canvas object.
"""
result = cls.fromLatticeAndShape(
Lat, T.TileShape, Seed=Seed, DefaultNN=DefaultNN
)
result.makePeriodic(T, Lat.getNeighbors)
return result
@classmethod
def fromLatticeAndTilingScan(cls, Lat, T, DefaultNN=0):
"""Make Canvas by iterating over Lattice points that fit in Tiling.
See documentation for fromLatticeAndTiling.
This constructor additionally makes the resulting Canvas periodic.
Args:
Lat(Lattice): Lattice with has defined Scan method.
T(Tiling): Tiling that provides a Polyhedron shape.
DefaultNN(int, optional): Number of neighbors to allocate
in neighborhood. (Default value = 0)
Returns:
Canvas: A new Canvas object.
"""
result = cls.fromLatticeAndShapeScan(Lat, T.TileShape, DefaultNN=DefaultNN)
result.makePeriodic(T, Lat.getNeighbors)
return result
# === ASSERTION OF CLASS DESIGN
def isConsistentWithDesign(self):
"""Determine if object is consistent with class assumptions."""
if len(self.Points) != len(self.NeighborhoodIndexes):
return False
return True
# === MANIPULATION METHODS
def addLocation(self, P, NNeighbors=None):
"""Add new location to Points.
Args:
P(numpy.ndarray): Point to add.
NNeighbors(int, optional): Number of neighbors to allocate
in a neighborhood. If None, use the instance default
self.DefaultNN (Default value = None)
Returns:
None.
"""
assert not self.hasPoint(P)
self._Points.append(P)
self._NeighborhoodIndexes.append([None] * (NNeighbors or self.__DefaultNN))
assert self.isConsistentWithDesign()
def setNeighbors(self, P1, P2, l=None):
"""Set a (directed) neighbor connection between two points.
Args:
P1(numpy.ndarray): Canvas Point to set neighbor for.
P2(numpy.ndarray): Canvas Point to set as neighbor.
l(int, optional): Index of neighbor to set. For example, if
l=3, then P2 is set to the fourth neighbor of P1.
If None, appends to the neighborhood.
(Default value = None)
Returns:
None.
"""
assert self.hasPoint(P1)
assert self.hasPoint(P2)
i = self.getPointIndex(P1)
j = self.getPointIndex(P2)
self.setNeighborsIJ(i, j, l=l)
assert self.isConsistentWithDesign()
def setNeighborsIJ(self, i, j, l=None):
"""Set a (directed) neighbor connection between two indices.
Args:
i(int): Index of location to set neighbor for.
j(int): Index of location to set as neighbor.
l(int, optional): Index of neighbor to set. For example, if
l=3, then j is set to the fourth neighbor of i.
If None, appends to the neighborhood.
(Default value = None)
Returns:
None.
"""
if l is None:
self._NeighborhoodIndexes[i].append(j)
else:
self._NeighborhoodIndexes[i][l] = j
assert self.isConsistentWithDesign()
def setNeighborLofI(self, PN, l, i, blnSetNoneOtherwise=True):
"""Set a (directed) neighbor connection between a Point and index.
Args:
blnSetNoneOtherwise:
i:
PN(numpy.ndarray): Canvas Point to set as neighbor.
l(int): Index of neighbor to set. For example, if
l=3, then PN is set as the fourth neighbor of i.
i(int): Index of location to set neighbor for.
blnSetNoneOtherwise(bool, optional): Flag to control behavior
if PN is not found in Canvas. (Default value = True)
Returns:
None.
"""
assert i < len(self._NeighborhoodIndexes)
assert l < len(self._NeighborhoodIndexes[i])
if self.hasPoint(PN):
self._NeighborhoodIndexes[i][l] = self.getPointIndex(PN)
elif blnSetNoneOtherwise:
self._NeighborhoodIndexes[i][l] = None
assert self.isConsistentWithDesign()
def setNeighborsOfI(self, PNs, i):
"""Set a list of points as neighbors to an index.
Args:
PNs(list<numpy.ndarray>): Points to set as neighbors of i.
i(int): Index to set neighbors for.
Returns:
None.
"""
self._NeighborhoodIndexes[i] = [None] * len(PNs)
for l, P in enumerate(PNs):
self.setNeighborLofI(P, l, i)
assert self.isConsistentWithDesign()
def setNeighborsFromFunc(self, NeighborsFunc):
"""Set neighbors across the Canvas from a functor.
Args:
NeighborsFunc(function): Function that takes as input a
point (numpy.ndarray) and returns a list of points.
Returns:
None.
"""
for i, P in enumerate(self.Points):
PNs = NeighborsFunc(P)
self.setNeighborsOfI(PNs, i)
assert self.isConsistentWithDesign()
def getNeighborsFromFunc(self, NeighborsFunc, layer=1):
"""Get neighbors across the Canvas from a functor.
Args:
NeighborsFunc(function): Function that takes as input a point
(numpy.ndarray) and returns a list of points.
layer(int): Target layer of neighbors.
Returns:
list<list<int>> Indexes matrix
"""
result = [[] for _ in range(len(self.Points))]
for i, P in enumerate(self.Points):
PNs = NeighborsFunc(P, layer)
result[i] = [None] * len(PNs)
for j, PN in enumerate(PNs):
if self.hasPoint(PN):
result[i][j] = self.getPointIndex(PN)
return result
def getNeighborsFromFuncAndTiling(self, NeighborsFunc, argTiling, layer=1):
"""Get neighbors across the Canvas from a functor and Tiling.
Args:
NeighborsFunc(function): Function that takes as input a point
(numpy.ndarray) and returns a list of points.
argTiling(Tiling): Tiling object that specifies the periodicity
of the Canvas.
layer(int): Target layer of neighbors.
Returns:
list<list<int>> Indexes matrix
"""
result = [[] for _ in range(len(self.Points))]
for i, P in enumerate(self.Points):
PNs = NeighborsFunc(P, layer)
result[i] = [None] * len(PNs)
for j, PN in enumerate(PNs):
if self.hasPoint(PN):
result[i][j] = self.getPointIndex(PN)
else:
for TilingDirection in argTiling.TilingDirections:
PtoTry = PN + TilingDirection
if self.hasPoint(PtoTry):
result[i][j] = self.getPointIndex(PtoTry)
return result
def makePeriodic(self, argTiling, NeighborsFunc):
"""Make connections periodic across the edges of Tiling.
Args:
argTiling(Tiling: Tiling): Tiling to provide TilingDirections.
NeighborsFunc(function): Function that takes as input a
point (numpy.ndarray) and returns a list of points.
Returns:
None.
"""
for i, P in enumerate(self.Points):
LatNeighbors = NeighborsFunc(P)
for l, Index in enumerate(self.NeighborhoodIndexes[i]):
if Index is None:
assert not self.hasPoint(
LatNeighbors[l]
) # else, Canvas constructed incorrectly
for TilingDirection in argTiling.TilingDirections:
PtoTry = LatNeighbors[l] + TilingDirection
if self.hasPoint(PtoTry):
self._NeighborhoodIndexes[i][l] = self.getPointIndex(PtoTry)
break
def addShells(self, n, NeighborsFunc):
"""Add locations in n-shells around current Points.
Args:
n(int): Number of shells to add.
NeighborsFunc(function): Function that takes as input a
point (numpy.ndarray) and returns a list of points.
Returns:
None.
"""
for _ in range(n):
self.addShell(NeighborsFunc)
def addShell(self, NeighborsFunc):
"""Add locations in a shell around current Points.
Args:
NeighborsFunc(function): Function that takes as input a
point (numpy.ndarray) and returns a list of points.
Returns:
None.
"""
Shell = self.getShell(NeighborsFunc)
for P in Shell:
self.addLocation(P)
self.setNeighborsFromFunc(NeighborsFunc)
def transform(self, TransF):
"""Transform the Points in Canvas according to a functor.
Args:
TransF(TransformFunc): Transformation to apply to Points.
Returns:
None.
"""
for P in self._Points:
TransF.transform(P)
def getTransformed(self, TransF):
"""Copy and transform this Canvas.
Args:
TransF(TransformFunc): Transformation to apply to Points.
Returns:
None.
"""
result = deepcopy(self)
result.transform(TransF)
return result
def addOther(self, other, blnAssertNotAlreadyInCanvas=True):
"""Add other Canvas to this one.
Args:
other(Canvas): Canvas to append.
blnAssertNotAlreadyInCanvas(bool, optional): Flag to enable
assertion that all locations were new and unique.
(Default value = True)
Returns:
None.
"""
for P in other.Points:
if blnAssertNotAlreadyInCanvas:
assert not self.hasPoint(P)
self.addLocation(P)
# === PROPERTY EVALUATION METHODS
def __len__(self):
"""Get the number of Points for this Canvas."""
return len(self.Points)
def __eq__(self, other):
"""Compare strict equality of Canvas data."""
return (
myPointsEq(self.Points, other.Points, Canvas.DBL_TOL)
and self.NeighborhoodIndexes == other.NeighborhoodIndexes
)
def hasPoint(self, P):
"""Identify if point is in Canvas.
Args:
P(numpy.ndarray): Point to identify in Canvas.
Returns:
bool) True if Points has P.
"""
for Q in self.Points:
# NOTE: There are several ways to test point membership.
# This is optimized for speed.
if myArrayEq(P, Q, Canvas.DBL_TOL):
return True
return False
def getPointIndex(self, P):
"""Identify the index of a point in the Canvas.
Args:
P(numpy.ndarray): Point in the Canvas.
Returns:
int) Index of P in Points.
"""
for i, Q in enumerate(self.Points):
# NOTE: There are several ways to test point membership.
# This is optimized for speed.
if myArrayEq(P, Q, Canvas.DBL_TOL):
return i
return None
def getNeighbors(self, P):
"""Identify set of neighbors to a point in Canvas.
Args:
P(numpy.ndarray): Point to get neighbors for.
Returns:
list<int>: Neighborhood of P.
"""
return self.NeighborhoodIndexes[self.getPointIndex(P)]
def getNeighborLofI(self, l, i):
"""Identify neighbor for specific index and neighbor order.
Args:
l(int): Order of neighbor in neighborhood
i(int): Index to consider the neighborhood of.
Returns:
int: Index for neighbor l of i.
"""
assert i < len(self.NeighborhoodIndexes)
assert l < len(self.NeighborhoodIndexes[i])
return self.NeighborhoodIndexes[i][l]
def getShell(self, NeighborsFunc):
"""Identify set of neighboring points not in Canvas.
Args:
NeighborsFunc(function): Function that takes as input a
point (numpy.ndarray) and returns a list of points.
Returns:
list<numpy.ndarray>: Set of points to consider as
neighboring shell.
"""
result = []
for i, P in enumerate(self.Points):
Neighs = NeighborsFunc(P)
for Neigh in Neighs:
if not self.hasPoint(Neigh) and not ListHasPoint(
result, Neigh, Canvas.DBL_TOL
):
result.append(Neigh)
return result
def getNeighborhoodIndexes(self, Lat, layer=1, T=None):
"""Wrapper of functions that returns neighbors across the Canvas
Args:
Lat(Lattice): Lattice of the Canvas
layer(int): optional, target layer of neighbors
T(Tiling): optional, Tiling object that specifies the periodicity
Returns:
list<list<int>> Indexes matrix
"""
if layer == 1:
return self.NeighborhoodIndexes
elif T is None:
return self.getNeighborsFromFunc(Lat.getNeighbors, layer)
else:
return self.getNeighborsFromFuncAndTiling(Lat.getNeighbors, T, layer)
# === BASIC QUERY METHODS
@property
def Points(self):
"""Get Points in Canvas."""
return self._Points
@property
def NeighborhoodIndexes(self):
"""Get description of neighborhoods in Canvas."""
return self._NeighborhoodIndexes
# === REPORTING METHODS
def printPoints(self):
"""Pretty-print Points."""
for i, P in enumerate(self.Points):
print("{}: {}".format(i, P))
def printNeighborhoodIndexes(self, layer=1):
"""Pretty-print NeighborhoodIndexes."""
for i, Neighborhood in enumerate(self.getNeighborhoodIndexes(layer)):
print("{}: {}".format(i, Neighborhood))