From a8eed3184b92e8df348138b9755cb8c120ed51cf Mon Sep 17 00:00:00 2001 From: Nick Papior Date: Tue, 26 Mar 2024 09:30:13 +0100 Subject: [PATCH] preliminary test for 0-orbital Atoms Signed-off-by: Nick Papior --- src/sisl/_core/atom.py | 88 +++++++++++--------- src/sisl/_core/sparse.py | 8 +- src/sisl/_core/tests/test_atom.py | 16 +++- src/sisl/_core/tests/test_sparse.py | 2 +- src/sisl/_core/tests/test_sparse_geometry.py | 4 +- src/sisl/_help.py | 4 + 6 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/sisl/_core/atom.py b/src/sisl/_core/atom.py index 0fc4ad59f0..d2b008a091 100644 --- a/src/sisl/_core/atom.py +++ b/src/sisl/_core/atom.py @@ -1076,7 +1076,7 @@ def __init__( if isinstance(orbitals, (tuple, list, np.ndarray)): if len(orbitals) == 0: # This may be the same as only regarding `R` argument - pass + orbitals = None elif isinstance(orbitals[0], Orbital): # all is good self._orbitals = orbitals @@ -1105,7 +1105,7 @@ def __init__( R = _a.asarrayd(kwargs["R"]).ravel() self._orbitals = [Orbital(r) for r in R] else: - self._orbitals = [Orbital(-1.0)] + self._orbitals = [] if mass is None: self._mass = _ptbl.atomic_mass(mass_Z) @@ -1247,39 +1247,40 @@ def __getattr__(self, attr): ---------- attr : str """ - # First we create a list of values that the orbitals have # Some may have it, others may not vals = [None] * len(self.orbitals) - found = False + found = True is_Integral = is_Real = is_callable = True for io, orb in enumerate(self.orbitals): try: vals[io] = getattr(orb, attr) - found = True is_callable &= callable(vals[io]) is_Integral &= isinstance(vals[io], Integral) is_Real &= isinstance(vals[io], Real) except AttributeError: - pass + found = False - if found == 0: + if not found: # we never got any values, reraise the AttributeError raise AttributeError( f"'{self.__class__.__name__}.orbitals' objects has no attribute '{attr}'" ) # Now parse the data, currently we'll only allow Integral, Real, Complex - if is_Integral: + if is_Integral and vals: + # we use and vals to ensure that emtpy arrays returns as reals for io in range(len(vals)): if vals[io] is None: vals[io] = 0 return _a.arrayi(vals) + elif is_Real: for io in range(len(vals)): if vals[io] is None: vals[io] = 0.0 return _a.arrayd(vals) + elif is_callable: def _ret_none(*args, **kwargs): @@ -1451,13 +1452,13 @@ class repeated `na` times. # Using the slots should make this class slightly faster. __slots__ = ("_atom", "_species", "_firsto") - def __init__(self, atoms: AtomsLike = "H", na: Optional[int] = None): + def __init__(self, atoms: Optional[AtomsLike] = None, na: Optional[int] = None): # Default value of the atom object if atoms is None: - atoms = Atom("H") + uatoms = [] + species = [] - # Correct the atoms input to Atom - if isinstance(atoms, Atom): + elif isinstance(atoms, Atom): uatoms = [atoms] species = [0] @@ -1493,7 +1494,9 @@ def __init__(self, atoms: AtomsLike = "H", na: Optional[int] = None): species.append(s) else: - raise ValueError(f"atoms keyword type is not acceptable {type(atoms)}") + raise ValueError( + f"{self.__class__.__name__}(atoms=) keyword type is not acceptable {type(atoms)}" + ) # Default for number of atoms if na is None: @@ -1512,6 +1515,16 @@ def _update_orbitals(self): uorbs = _a.arrayi([a.no for a in self.atom]) self._firsto = np.insert(_a.cumsumi(uorbs[self.species]), 0, 0) + def __str__(self): + """Return the `Atoms` in str""" + s = f"{self.__class__.__name__}{{species: {len(self._atom)},\n" + for a, idx in self.iter(True): + s += " {1}: {0},\n".format(len(idx), str(a).replace("\n", "\n ")) + return f"{s}}}" + + def __repr__(self): + return f"<{self.__module__}.{self.__class__.__name__} nspecies={len(self._atom)}, na={len(self)}, no={self.no}>" + @property def atom(self): """List of unique atoms in this group of atoms""" @@ -1539,6 +1552,15 @@ def specie(self): """List of atomic species""" return self._species + def __len__(self): + """Return number of atoms in the object""" + return len(self._species) + + @property + def na(self): + """Return number of atoms in the object (same as len)""" + return len(self.species) + @property def no(self): """Total number of orbitals in this list of atoms""" @@ -1566,6 +1588,18 @@ def q0(self): q0 = _a.arrayd([a.q0.sum() for a in self.atom]) return q0[self.species] + @property + def mass(self): + """Array of masses of the contained objects""" + umass = _a.arrayd([a.mass for a in self.atom]) + return umass[self.species] + + @property + def Z(self): + """Array of atomic numbers""" + uZ = _a.arrayi([a.Z for a in self.atom]) + return uZ[self.species] + def orbital(self, io): """Return an array of orbital of the contained objects""" io = _a.asarrayi(io) @@ -1594,18 +1628,6 @@ def maxR(self, all=False): return maxR[self.species] return np.amax([a.maxR() for a in self.atom]) - @property - def mass(self): - """Array of masses of the contained objects""" - umass = _a.arrayd([a.mass for a in self.atom]) - return umass[self.species] - - @property - def Z(self): - """Array of atomic numbers""" - uZ = _a.arrayi([a.Z for a in self.atom]) - return uZ[self.species] - def index(self, atom): """Return the indices of the atom object""" return (self._species == self.species_index(atom)).nonzero()[0] @@ -1774,7 +1796,7 @@ def swap_atom(self, a, b): return atoms def reverse(self, atoms=None): - """Returns a reversed geometry + """Returns a reversed atoms object Also enables reversing a subset of the atoms. """ @@ -1786,20 +1808,6 @@ def reverse(self, atoms=None): copy._update_orbitals() return copy - def __str__(self): - """Return the `Atoms` in str""" - s = f"{self.__class__.__name__}{{species: {len(self._atom)},\n" - for a, idx in self.iter(True): - s += " {1}: {0},\n".format(len(idx), str(a).replace("\n", "\n ")) - return f"{s}}}" - - def __repr__(self): - return f"<{self.__module__}.{self.__class__.__name__} nspecies={len(self._atom)}, na={len(self)}, no={self.no}>" - - def __len__(self): - """Return number of atoms in the object""" - return len(self._species) - def iter(self, species=False): """Loop on all atoms diff --git a/src/sisl/_core/sparse.py b/src/sisl/_core/sparse.py index 532943e919..9efc564984 100644 --- a/src/sisl/_core/sparse.py +++ b/src/sisl/_core/sparse.py @@ -261,10 +261,10 @@ def __init_shape(self, arg1, dim=1, dtype=None, nnzpr=20, nnz=None, **kwargs): # unpack size and check the sizes are "physical" M, N, K = arg1 - if M <= 0 or N <= 0 or K <= 0: + if M < 0 or N < 0 or K < 0: raise ValueError( - self.__class__.__name__ - + f" invalid size of sparse matrix, one of the dimensions is zero: M={M}, N={N}, K={K}" + f"{self.__class__.__name__} invalid size of sparse matrix. " + f"Must have finite (or zero) dimension." ) # Store shape @@ -278,7 +278,7 @@ def __init_shape(self, arg1, dim=1, dtype=None, nnzpr=20, nnz=None, **kwargs): # number of non-zero elements is NOT given nnz = M * nnzpr - else: + elif M > 0: # number of non-zero elements is give AND larger # than the provided non-zero elements per row nnzpr = nnz // M diff --git a/src/sisl/_core/tests/test_atom.py b/src/sisl/_core/tests/test_atom.py index 585da901c1..35022fd0b1 100644 --- a/src/sisl/_core/tests/test_atom.py +++ b/src/sisl/_core/tests/test_atom.py @@ -42,6 +42,19 @@ def test_atom_simple(setup): assert setup.Au == setup.Au.copy() +def test_atom_empty(): + atom = Atom("C") + assert len(atom) == 0 + assert atom.no == 0 + for attr in ("orbitals", "R", "q0"): + assert len(getattr(atom, attr)) == 0 + atoms = Atoms() + assert atoms.no == 0 + assert atoms.nspecies == 0 + for attr in ("lasto", "orbitals", "mass", "Z"): + assert getattr(atoms, attr).size == 0 + + def test_atom_ghost(): assert isinstance(Atom[1], Atom) assert isinstance(Atom[-1], AtomGhost) @@ -110,8 +123,7 @@ def test4(setup): def test5(setup): - assert Atom(Z=1, mass=12).R < 0 - assert Atom(Z=1, mass=12).R.size == 1 + assert Atom(Z=1, mass=12).R.size == 0 assert Atom(Z=1, mass=12).mass == 12 assert Atom(Z=31, mass=12).mass == 12 assert Atom(Z=31, mass=12).Z == 31 diff --git a/src/sisl/_core/tests/test_sparse.py b/src/sisl/_core/tests/test_sparse.py index ec58eb9c78..b73b6cfa46 100644 --- a/src/sisl/_core/tests/test_sparse.py +++ b/src/sisl/_core/tests/test_sparse.py @@ -58,7 +58,7 @@ def test_fail_init1(): def test_fail_init_shape0(): with pytest.raises(ValueError): - SparseCSR((0, 10, 10), dtype=np.int32) + SparseCSR((-1, 10, 10), dtype=np.int32) def test_fail_init2(): diff --git a/src/sisl/_core/tests/test_sparse_geometry.py b/src/sisl/_core/tests/test_sparse_geometry.py index a688486b34..9869497357 100644 --- a/src/sisl/_core/tests/test_sparse_geometry.py +++ b/src/sisl/_core/tests/test_sparse_geometry.py @@ -259,8 +259,8 @@ def test_sp_orb_remove(self, setup): assert so.geometry.na - 1 == so2.geometry.na def test_sp_orb_remove_atom(self): - so = SparseOrbital(Geometry([[0] * 3, [1] * 3], [Atom[1], Atom[2]], 2)) - so2 = so.remove(Atom[1]) + so = SparseOrbital(Geometry([[0] * 3, [1] * 3], [Atom(1, 1), Atom(2, 1)], 2)) + so2 = so.remove(Atom(1, 1)) assert so.geometry.na - 1 == so2.geometry.na assert so.geometry.no - 1 == so2.geometry.no diff --git a/src/sisl/_help.py b/src/sisl/_help.py index ae22214ce6..292515405b 100644 --- a/src/sisl/_help.py +++ b/src/sisl/_help.py @@ -45,6 +45,10 @@ def array_fill_repeat(array, size, axis=-1, cls=None): to be an integer part of `size`. """ array = np.asarray(array, dtype=cls) + if size == 0 or array.size == 0: + if cls is None: + cls = array.dtype + return np.empty([0], dtype=cls) try: reps = size // array.shape[axis] except Exception: