-
-
Notifications
You must be signed in to change notification settings - Fork 58
/
geometry.py
4379 lines (3667 loc) · 149 KB
/
geometry.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# To check for integers
from __future__ import annotations
import logging
import warnings
from collections import OrderedDict
from functools import singledispatchmethod
from itertools import product
from math import acos
from numbers import Integral, Real
from pathlib import Path
from typing import Iterator, List, Optional, Sequence, Tuple, Union
import numpy as np
from numpy import (
argsort,
bool_,
ceil,
concatenate,
diff,
dot,
floor,
int32,
isin,
ndarray,
split,
sqrt,
square,
tile,
unique,
)
import sisl._array as _a
from sisl._category import Category, GenericCategory
from sisl._dispatch_class import _Dispatchs
from sisl._dispatcher import AbstractDispatch, ClassDispatcher, TypeDispatcher
from sisl._help import isndarray
from sisl._indices import (
indices_gt_le,
indices_in_sphere_with_dist,
indices_le,
list_index_le,
)
from sisl._internal import set_module
from sisl._math_small import cross3, is_ascending
from sisl._namedindex import NamedIndex
from sisl.messages import SislError, deprecate_argument, info, warn
from sisl.shape import Cube, Shape, Sphere
from sisl.typing import ArrayLike, AtomsIndex, NDArray, OrbitalsIndex, SileLike
from sisl.utils import (
angle,
cmd,
default_ArgumentParser,
default_namespace,
direction,
lstranges,
str_spec,
strmap,
)
from sisl.utils.mathematics import fnorm
from .atom import Atom, Atoms
from .lattice import Lattice, LatticeChild
from .orbital import Orbital
__all__ = ["Geometry", "sgeom", "AtomCategory"]
_log = logging.getLogger(__name__)
# It needs to be here otherwise we can't use it in these routines
# Note how we are overwriting the module
@set_module("sisl.geom")
class AtomCategory(Category):
__slots__ = ()
@classmethod
def is_class(cls, name, case=True) -> bool:
# Strip off `Atom`
cls_name = cls.__name__[4:]
if case:
return cls_name == name
return cls_name.lower() == name.lower()
@set_module("sisl")
class Geometry(
LatticeChild,
_Dispatchs,
dispatchs=[
ClassDispatcher("new", obj_getattr="error", instance_dispatcher=TypeDispatcher),
ClassDispatcher("to", obj_getattr="error", type_dispatcher=None),
],
when_subclassing="copy",
):
"""Holds atomic information, coordinates, species, lattice vectors
The `Geometry` class holds information regarding atomic coordinates,
the atomic species, the corresponding lattice-vectors.
It enables the interaction and conversion of atomic structures via
simple routine methods.
All lengths are assumed to be in units of Angstrom, however, as
long as units are kept same the exact units are irrespective.
.. code:: python
>>> square = Geometry([[0.5, 0.5, 0.5]], Atom(1),
... lattice=Lattice([1, 1, 10], nsc=[3, 3, 1]))
>>> print(square)
Geometry{na: 1, no: 1,
Atoms{species: 1,
Atom{H, Z: 1, mass(au): 1.00794, maxR: -1.00000,
Orbital{R: -1.00000, q0: 0.0}
}: 1,
},
maxR: -1.00000,
Lattice{volume: 1.0000e+01, nsc: [3 3 1]}
}
Parameters
----------
xyz : array_like
atomic coordinates
``xyz[i, :]`` is the atomic coordinate of the i'th atom.
atoms : array_like or Atoms
atomic species retrieved from the `PeriodicTable`
lattice : Lattice
the unit-cell describing the atoms in a periodic
super-cell
Examples
--------
An atomic cubic lattice of Hydrogen atoms
>>> xyz = [[0, 0, 0],
... [1, 1, 1]]
>>> sc = Lattice([2,2,2])
>>> g = Geometry(xyz, Atom('H'), sc)
The following estimates the lattice vectors from the
atomic coordinates, although possible, it is not recommended
to be used.
>>> xyz = [[0, 0, 0],
... [1, 1, 1]]
>>> g = Geometry(xyz, Atom('H'))
Conversion of geometries to other projects instances can be done via
sisl's dispatch functionality
>>> g.to.ase()
Atoms(...)
converts to an ASE `ase.Atoms` object.
See Also
--------
Atoms : contained atoms ``self.atoms``
Atom : contained atoms are each an object of this
"""
@deprecate_argument(
"sc",
"lattice",
"argument sc has been deprecated in favor of lattice, please update your code.",
"0.15",
"0.16",
)
def __init__(self, xyz: ArrayLike, atoms=None, lattice=None, names=None):
# Create the geometry coordinate, be aware that we do not copy!
self.xyz = _a.asarrayd(xyz, order="C").reshape(-1, 3)
# Default value
if atoms is None:
atoms = Atom("H")
# Create the local Atoms object
self._atoms = Atoms(atoms, na=self.na)
# Assign a group specifier
if isinstance(names, NamedIndex):
self._names = names.copy()
else:
self._names = NamedIndex(names)
self._init_lattice(lattice)
def _init_lattice(self, lattice):
"""Initializes the supercell by *calculating* the size if not supplied
If the supercell has not been passed we estimate the unit cell size
by calculating the bond-length in each direction for a square
Cartesian coordinate system.
"""
# We still need the *default* super cell for
# estimating the supercell
self.set_lattice(lattice)
if lattice is not None:
return
# First create an initial guess for the supercell
# It HAS to be VERY large to not interact
closest = self.close(0, R=(0.0, 0.4, 5.0))[2]
if len(closest) < 1:
# We could not find any atoms very close,
# hence we simply return and now it becomes
# the users responsibility
# We create a molecule box with +10 A in each direction
m, M = np.amin(self.xyz, axis=0), np.amax(self.xyz, axis=0) + 10.0
self.set_lattice(M - m)
return
sc_cart = _a.zerosd([3])
cart = _a.zerosd([3])
for i in range(3):
# Initialize cartesian direction
cart[i] = 1.0
# Get longest distance between atoms
max_dist = np.amax(self.xyz[:, i]) - np.amin(self.xyz[:, i])
dist = self.xyz[closest, :] - self.xyz[0, :][None, :]
# Project onto the direction
dd = np.abs(dot(dist, cart))
# Remove all below .4
tmp_idx = (dd >= 0.4).nonzero()[0]
if len(tmp_idx) > 0:
# We have a success
# Add the bond-distance in the Cartesian direction
# to the maximum distance in the same direction
sc_cart[i] = max_dist + np.amin(dd[tmp_idx])
else:
# Default to LARGE array so as no
# interaction occurs (it may be 2D)
sc_cart[i] = max(10.0, max_dist)
cart[i] = 0.0
# Re-set the supercell to the newly found one
self.set_lattice(sc_cart)
@property
def atoms(self) -> Atoms:
"""Atoms associated with the geometry"""
return self._atoms
@property
def names(self):
"""The named index specifier"""
return self._names
@property
def q0(self) -> float:
"""Total initial charge in this geometry (sum of q0 off all atoms)"""
return self.atoms.q0.sum()
@property
def mass(self) -> ndarray:
"""The mass of all atoms as an array"""
return self.atoms.mass
def maxR(self, all: bool = False) -> float:
"""Maximum orbital range of the atoms"""
return self.atoms.maxR(all)
@property
def na(self) -> int:
"""Number of atoms in geometry"""
return self.xyz.shape[0]
@property
def na_s(self) -> int:
"""Number of supercell atoms"""
return self.na * self.n_s
def __len__(self) -> int:
"""Number of atoms in geometry in unit cell"""
return self.na
@property
def no(self) -> int:
"""Number of orbitals in unit cell"""
return self.atoms.no
@property
def no_s(self) -> int:
"""Number of supercell orbitals"""
return self.no * self.n_s
@property
def firsto(self) -> NDArray[np.int32]:
"""The first orbital on the corresponding atom"""
return self.atoms.firsto
@property
def lasto(self) -> NDArray[np.int32]:
"""The last orbital on the corresponding atom"""
return self.atoms.lasto
@property
def orbitals(self) -> ndarray:
"""List of orbitals per atom"""
return self.atoms.orbitals
## End size of geometry
@property
def fxyz(self) -> NDArray[np.float64]:
"""Returns geometry coordinates in fractional coordinates"""
return dot(self.xyz, self.icell.T)
def __setitem__(self, atoms, value):
"""Specify geometry coordinates"""
if isinstance(atoms, str):
self.names.add_name(atoms, value)
elif isinstance(value, str):
self.names.add_name(value, atoms)
@singledispatchmethod
def __getitem__(self, atoms) -> ndarray:
"""Geometry coordinates (allows supercell indices)"""
return self.axyz(atoms)
@__getitem__.register
def _(self, atoms: slice) -> ndarray:
if atoms.stop is None:
atoms = atoms.indices(self.na)
else:
atoms = atoms.indices(self.na_s)
return self.axyz(_a.arangei(atoms[0], atoms[1], atoms[2]))
@__getitem__.register
def _(self, atoms: tuple) -> ndarray:
return self[atoms[0]][..., atoms[1]]
@singledispatchmethod
def _sanitize_atoms(self, atoms) -> ndarray:
"""Converts an `atoms` to index under given inputs
`atoms` may be one of the following:
- boolean array -> nonzero()[0]
- name -> self._names[name]
- `Atom` -> self.atoms.index(atom)
- range/list/ndarray -> ndarray
"""
if atoms is None:
return np.arange(self.na)
atoms = _a.asarray(atoms)
if atoms.size == 0:
return _a.asarrayl([])
if atoms.dtype == bool_:
return atoms.nonzero()[0]
return atoms
@_sanitize_atoms.register
def _(self, atoms: ndarray) -> ndarray:
if atoms.dtype == bool_:
return np.flatnonzero(atoms)
return atoms
@_sanitize_atoms.register
def _(self, atoms: slice) -> ndarray:
# TODO consider doing range(self.na)[atoms]
start, stop, step = atoms.start, atoms.stop, atoms.step
if start is None:
start = 0
if stop is None:
stop = self.na
if step is None:
step = 1
return np.arange(start, stop, step)
@_sanitize_atoms.register
def _(self, atoms: str) -> ndarray:
return self.names[atoms]
@_sanitize_atoms.register
def _(self, atoms: Atom) -> ndarray:
return self.atoms.index(atoms)
@_sanitize_atoms.register(AtomCategory)
@_sanitize_atoms.register(GenericCategory)
def _(
self,
atoms_: Union[AtomCategory, GenericCategory],
atoms: AtomsIndex = None,
) -> ndarray:
# First do categorization
cat = atoms_.categorize(self, atoms)
def m(cat):
for ia, c in enumerate(cat):
if c == None:
# we are using NullCategory == None
pass
else:
yield ia
return _a.fromiterl(m(cat))
@_sanitize_atoms.register
def _(self, atoms_: dict, atoms: AtomsIndex = None) -> ndarray:
# First do categorization
return self._sanitize_atoms(AtomCategory.kw(**atoms_), atoms)
@_sanitize_atoms.register
def _(self, atoms: Shape) -> ndarray:
# This is perhaps a bit weird since a shape could
# extend into the supercell.
# Since the others only does this for unit-cell atoms
# then it seems natural to also do that here...
return atoms.within_index(self.xyz)
@_sanitize_atoms.register
def _(self, atoms: bool) -> ndarray:
if atoms:
return np.arange(self.na)
return np.array([], np.int64)
@singledispatchmethod
def _sanitize_orbs(self, orbitals) -> ndarray:
"""Converts an `orbital` to index under given inputs
`orbital` may be one of the following:
- boolean array -> nonzero()[0]
- dict -> {atom: sub_orbital}
"""
if orbitals is None:
return np.arange(self.no)
orbitals = _a.asarray(orbitals)
if orbitals.size == 0:
return _a.asarrayl([])
elif orbitals.dtype == np.bool_:
return orbitals.nonzero()[0]
return orbitals
@_sanitize_orbs.register
def _(self, orbitals: ndarray) -> ndarray:
if orbitals.dtype == bool_:
return np.flatnonzero(orbitals)
return orbitals
@_sanitize_orbs.register
def _(self, orbitals: slice) -> ndarray:
start, stop, step = orbitals.start, orbitals.stop, orbitals.step
if start is None:
start = 0
if stop is None:
stop = self.na
if step is None:
step = 1
return np.arange(start, stop, step)
@_sanitize_orbs.register
def _(self, orbitals: str) -> ndarray:
atoms = self._sanitize_atoms(orbitals)
return self.a2o(atoms, all=True)
@_sanitize_orbs.register
def _(self, orbitals: Atom) -> ndarray:
atoms = self._sanitize_atoms(orbitals)
return self.a2o(atoms, all=True)
@_sanitize_orbs.register
def _(self, orbitals: AtomCategory) -> ndarray:
atoms = self._sanitize_atoms(orbitals)
return self.a2o(atoms, all=True)
@_sanitize_orbs.register
def _(self, orbitals: Shape) -> ndarray:
atoms = self._sanitize_atoms(orbitals)
return self.a2o(atoms, all=True)
@_sanitize_orbs.register
def _(self, orbitals: dict) -> ndarray:
"""A dict has atoms as keys"""
def conv(atom, orbs):
atom = self._sanitize_atoms(atom)
return np.add.outer(self.firsto[atom], orbs).ravel()
return np.concatenate(
tuple(conv(atom, orbs) for atom, orbs in orbitals.items())
)
@_sanitize_orbs.register
def _(self, orbitals: bool) -> ndarray:
if orbitals:
return np.arange(self.no)
return np.array([], dtype=np.int64)
def as_primary(
self, na_primary: int, axes: Sequence[int] = (0, 1, 2), ret_super: bool = False
) -> Union[Geometry, Tuple[Geometry, Lattice]]:
"""Reduce the geometry to the primary unit-cell comprising `na_primary` atoms
This will basically try and find the tiling/repetitions required for the geometry to only have
`na_primary` atoms in the unit cell.
Parameters
----------
na_primary :
number of atoms in the primary unit cell
axes :
only search the given directions for supercells, default to all directions
ret_super :
also return the number of supercells used in each direction
Returns
-------
`Geometry`
the primary unit cell
`Lattice`
the tiled supercell numbers used to find the primary unit cell (only if `ret_super` is true)
Raises
------
`SislError`
If the algorithm fails.
"""
na = len(self)
if na % na_primary != 0:
raise ValueError(
f"{self.__class__.__name__}.as_primary requires the number of atoms to be divisable by the "
"total number of atoms."
)
axes = _a.arrayi(axes)
n_supercells = len(self) // na_primary
if n_supercells == 1:
# Return a copy of self
if ret_super:
return self.copy(), self.nsc.copy()
return self.copy()
# Now figure out the repetitions along each direction
fxyz = self.fxyz
# Move to 0
fxyz -= fxyz.min(0)
# Shift a little bit in to account for inaccuracies.
fxyz += (0.5 - (fxyz.max(0) - fxyz.min(0)) / 2) * 0.01
# Default guess to 1 along all directions
supercell = _a.onesi(3)
n_bin = n_supercells
while n_bin > 1:
# Create bins
bins = np.linspace(0, 1, n_bin + 1)
# Loop directions where we need to check
for axis in axes:
if supercell[axis] != 1:
continue
# A histogram should yield an equal splitting for each bins
# if the geometry is a n_bin repetition along the i'th direction.
# Hence if diff == 0 for all elements we have a match.
diff_bin = np.diff(np.histogram(fxyz[:, axis], bins)[0])
if diff_bin.sum() == 0:
supercell[axis] = n_bin
if np.prod(supercell) > n_supercells:
# For geometries with more than 1 atom in the primary unit cell
# we can get false positives (each layer can be split again)
# We will search again the max-value supercell
i_max = supercell.argmax()
n_bin = supercell[i_max]
supercell[i_max] = 1
# Quick escape if hit the correct number of supercells
if np.prod(supercell) == n_supercells:
break
n_bin -= 1
# Check that the number of supercells match
if np.prod(supercell) != n_supercells:
raise SislError(
f"{self.__class__.__name__}.as_primary could not determine the optimal supercell."
)
# Cut down the supercell (TODO this does not correct the number of supercell connections!)
lattice = self.lattice.copy()
for i in range(3):
lattice = lattice.untile(supercell[i], i)
# Now we need to find the atoms that are in the primary cell
# We do this by finding all coordinates within the primary unit-cell
fxyz = dot(self.xyz, lattice.icell.T)
# Move to 0 and shift in 0.05 Ang in each direction
fxyz -= fxyz.min(0)
# Find minimal distance in each direction
sc_idx = (supercell > 1).nonzero()[0]
min_fxyz = _a.zerosd(3)
for i in sc_idx:
s_fxyz = np.sort(fxyz[:, i])
min_fxyz[i] = s_fxyz[(s_fxyz < 1e-4).nonzero()[0][-1] + 1]
fxyz += min_fxyz * 0.05
# Find all fractional indices that are below 1
ind = np.logical_and.reduce(fxyz < 1.0, axis=1).nonzero()[0]
geom = self.sub(ind)
geom.set_lattice(lattice)
if ret_super:
return geom, supercell
return geom
def as_supercell(self) -> Geometry:
"""Create a new geometry equivalent to ``self * self.nsc``, where the indices are ordered as the supercells
Returns
-------
`Geometry`
the supercell expanded and reordered Geometry
"""
# Get total number of atoms
na = len(self)
# create the big supercell geometry in the simplest (linear) way
sc = self * self.nsc
# remove nsc, this supercell should hold all information
sc.set_nsc([1, 1, 1])
# get off-set for first atom
# this is used to correct the indices created after having shifted
# everything
f0 = self.fxyz[0]
# translate the supercell such that the 0, 0, 0 (primary cell)
# is located at the origin.
sc = sc.translate(-(self.nsc // 2) @ self.cell)
# Calculate the translation table such that the ordering in `sc` can
# be made to look like the `self` supercell indices
isc_sc = np.rint(sc.xyz[::na] @ self.icell.T - f0).astype(np.int32)
isc_self = self.a2isc(np.arange(self.n_s) * na)
def new_sub(isc):
return (abs(isc_sc - isc).sum(1) == 0).nonzero()[0][0]
# Create the translation table for the indices
translate = np.array([new_sub(isc) for isc in isc_self])
# make sure all atoms are present
translate = np.repeat(translate * na, na).reshape(-1, na) + np.arange(na)
# re-arrange the atoms and return
return sc.sub(translate.ravel())
def reorder(self) -> None:
"""Reorders atoms according to first occurence in the geometry
The atoms gets reordered according to their placement in the geometry.
For instance, if the first atom is the 2nd species in the geometry. Then
this routine will swap the 2nd and 1st species in the `self.atoms` object.
Notes
-----
This is an in-place operation.
"""
self._atoms = self.atoms.reorder(inplace=True)
def reduce(self) -> None:
"""Remove all atoms not currently used in the ``self.atoms`` object
Notes
-----
This is an in-place operation.
"""
self._atoms = self.atoms.reduce(inplace=True)
def rij(self, ia: AtomsIndex, ja: AtomsIndex) -> ndarray:
r"""Distance between atom `ia` and `ja`, atoms can be in super-cell indices
Returns the distance between two atoms:
.. math::
r^{IJ} = |\mathbf r^J - \mathbf r^I|
Parameters
----------
ia :
atomic index of first atom
ja :
atomic indices
"""
R = self.Rij(ia, ja)
if len(R.shape) == 1:
return (R[0] ** 2.0 + R[1] ** 2 + R[2] ** 2) ** 0.5
return fnorm(R)
def Rij(self, ia: AtomsIndex, ja: AtomsIndex) -> ndarray:
r"""Vector between atom `ia` and `ja`, atoms can be in super-cell indices
Returns the vector between two atoms:
.. math::
\mathbf r^{IJ} = \mathbf r^J - \mathbf r^I
Parameters
----------
ia :
atomic index of first atom
ja :
atomic indices
"""
xi = self.axyz(ia)
xj = self.axyz(ja)
if isinstance(ja, Integral):
return xj[:] - xi[:]
elif np.allclose(xi.shape, xj.shape):
return xj - xi
return xj - xi[None, :]
def orij(self, orbitals1: OrbitalsIndex, orbitals2: OrbitalsIndex) -> ndarray:
r"""Distance between orbital `orbitals1` and `orbitals2`, orbitals can be in super-cell indices
Returns the distance between two orbitals:
.. math::
r^{ij} = |\mathbf r^j - \mathbf r^i|
Parameters
----------
orbitals1 :
orbital index of first orbital
orbitals2 :
orbital indices
"""
return self.rij(self.o2a(orbitals1), self.o2a(orbitals2))
def oRij(self, orbitals1: OrbitalsIndex, orbitals2: OrbitalsIndex) -> ndarray:
r"""Vector between orbital `orbitals1` and `orbitals2`, orbitals can be in super-cell indices
Returns the vector between two orbitals:
.. math::
\mathbf r^{ij} = \mathbf r^j - \mathbf r^i
Parameters
----------
orbitals1 :
orbital index of first orbital
orbitals2 :
orbital indices
"""
return self.Rij(self.o2a(orbitals1), self.o2a(orbitals2))
@staticmethod
def read(sile: SileLike, *args, **kwargs) -> Geometry:
"""Reads geometry from the `Sile` using `Sile.read_geometry`
Parameters
----------
sile :
a `Sile` object which will be used to read the geometry
if it is a string it will create a new sile using `get_sile`.
See Also
--------
write : writes a `Geometry` to a given `Sile`/file
"""
# This only works because, they *must*
# have been imported previously
from sisl.io import BaseSile, get_sile
if isinstance(sile, BaseSile):
return sile.read_geometry(*args, **kwargs)
else:
with get_sile(sile, mode="r") as fh:
return fh.read_geometry(*args, **kwargs)
def __str__(self) -> str:
"""str of the object"""
s = f"{self.__class__.__name__}{{na: {self.na}, no: {self.no},\n "
s += str(self.atoms).replace("\n", "\n ")
if len(self.names) > 0:
s += ",\n " + str(self.names).replace("\n", "\n ")
return (
s
+ ",\n maxR: {0:.5f},\n {1}\n}}".format(
self.maxR(), str(self.lattice).replace("\n", "\n ")
)
).strip()
def __repr__(self) -> str:
"""A simple, short string representation."""
return f"<{self.__module__}.{self.__class__.__name__} na={self.na}, no={self.no}, nsc={self.nsc}>"
def iter(self) -> Iterator[int]:
"""An iterator over all atomic indices
This iterator is the same as:
>>> for ia in range(len(self)):
... <do something>
or equivalently
>>> for ia in self:
... <do something>
See Also
--------
iter_species : iterate across indices and atomic species
iter_orbitals : iterate across atomic indices and orbital indices
"""
yield from range(len(self))
__iter__ = iter
def iter_species(self, atoms: AtomsIndex = None) -> Iterator[int, Atom, int]:
"""Iterator over all atoms (or a subset) and species as a tuple in this geometry
>>> for ia, a, idx_species in self.iter_species():
... isinstance(ia, int) == True
... isinstance(a, Atom) == True
... isinstance(idx_species, int) == True
with ``ia`` being the atomic index, ``a`` the `Atom` object, ``idx_species``
is the index of the specie
Parameters
----------
atoms :
only loop on the given atoms, default to all atoms
See Also
--------
iter : iterate over atomic indices
iter_orbitals : iterate across atomic indices and orbital indices
"""
if atoms is None:
for ia in self:
yield ia, self.atoms[ia], self.atoms.species[ia]
else:
for ia in self._sanitize_atoms(atoms).ravel():
yield ia, self.atoms[ia], self.atoms.species[ia]
def iter_orbitals(
self, atoms: AtomsIndex = None, local: bool = True
) -> Iterator[int, int]:
r"""Returns an iterator over all atoms and their associated orbitals
>>> for ia, io in self.iter_orbitals():
with ``ia`` being the atomic index, ``io`` the associated orbital index on atom ``ia``.
Note that ``io`` will start from ``0``.
Parameters
----------
atoms :
only loop on the given atoms, default to all atoms
local :
whether the orbital index is the global index, or the local index relative to
the atom it resides on.
Yields
------
ia
atomic index
io
orbital index
See Also
--------
iter : iterate over atomic indices
iter_species : iterate across indices and atomic species
"""
if atoms is None:
if local:
for ia, IO in enumerate(zip(self.firsto, self.lasto + 1)):
for io in range(IO[1] - IO[0]):
yield ia, io
else:
for ia, IO in enumerate(zip(self.firsto, self.lasto + 1)):
for io in range(IO[0], IO[1]):
yield ia, io
else:
atoms = self._sanitize_atoms(atoms).ravel()
if local:
for ia, io1, io2 in zip(
atoms, self.firsto[atoms], self.lasto[atoms] + 1
):
for io in range(io2 - io1):
yield ia, io
else:
for ia, io1, io2 in zip(
atoms, self.firsto[atoms], self.lasto[atoms] + 1
):
for io in range(io1, io2):
yield ia, io
def iR(self, na: int = 1000, iR: int = 20, R: Optional[float] = None) -> int:
"""Return an integer number of maximum radii (``self.maxR()``) which holds approximately `na` atoms
Parameters
----------
na :
number of atoms within the radius
iR :
initial `iR` value, which the sphere is estitametd from
R :
the value used for atomic range (defaults to ``self.maxR()``)
Returns
-------
int
number of radius needed to contain `na` atoms. Minimally 2 will be returned.
"""
ia = np.random.randint(len(self))
# default block iterator
if R is None:
R = self.maxR() + 0.001
if R < 0:
raise ValueError(
f"{self.__class__.__name__}.iR unable to determine a number of atoms within a sphere with negative radius, is maxR() defined?"
)
# Number of atoms within 20 * R
naiR = max(1, len(self.close(ia, R=R * iR)))
# Convert to na atoms spherical radii
iR = int(4 / 3 * np.pi * R**3 / naiR * na)
return max(2, iR)
def iter_block_rand(
self,
iR: int = 20,
R: Optional[float] = None,
atoms: AtomsIndex = None,
) -> Iterator[Tuple[ndarray, ndarray]]:
"""Perform the *random* block-iteration by randomly selecting the next center of block"""
# We implement yields as we can then do nested iterators
# create a boolean array
na = len(self)
if atoms is not None:
not_passed = np.zeros(na, dtype=bool)
# Reverse the values
not_passed[atoms] = True
else:
not_passed = np.ones(na, dtype=bool)
# Figure out how many we need to loop on
not_passed_N = np.sum(not_passed)
if iR < 2:
raise SislError(f"{self.__class__.__name__}.iter_block_rand too small iR!")
if R is None:
R = self.maxR() + 0.001
# The boundaries (ensure complete overlap)
R = np.array([iR - 0.5, iR + 0.501]) * R
# loop until all passed are true
while not_passed_N > 0:
# Take a random non-passed element
all_true = not_passed.nonzero()[0]
# Shuffle should increase the chance of hitting a
# completely "fresh" segment, thus we take the most
# atoms at any single time.
# Shuffling will cut down needed iterations.
np.random.shuffle(all_true)
# take one element, after shufling, we can take the first
idx = all_true[0]
del all_true
# Now we have found a new index, from which
# we want to create the index based stuff on
# get all elements within two radii
all_idx = self.close(idx, R=R)
# Get unit-cell atoms, we are drawing a circle, and this
# circle only encompasses those already in the unit-cell.
all_idx[1] = np.union1d(
self.sc2uc(all_idx[0], unique=True), self.sc2uc(all_idx[1], unique=True)
)