biotite 1.2.0__cp312-cp312-win_amd64.whl → 1.4.0__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. biotite/application/viennarna/rnaplot.py +7 -7
  2. biotite/interface/openmm/__init__.py +4 -0
  3. biotite/interface/pymol/__init__.py +3 -0
  4. biotite/interface/pymol/object.py +3 -1
  5. biotite/interface/rdkit/__init__.py +4 -0
  6. biotite/interface/rdkit/mol.py +5 -5
  7. biotite/interface/version.py +23 -0
  8. biotite/sequence/align/banded.cp312-win_amd64.pyd +0 -0
  9. biotite/sequence/align/banded.pyx +1 -1
  10. biotite/sequence/align/kmeralphabet.cp312-win_amd64.pyd +0 -0
  11. biotite/sequence/align/kmersimilarity.cp312-win_amd64.pyd +0 -0
  12. biotite/sequence/align/kmertable.cp312-win_amd64.pyd +0 -0
  13. biotite/sequence/align/localgapped.cp312-win_amd64.pyd +0 -0
  14. biotite/sequence/align/localungapped.cp312-win_amd64.pyd +0 -0
  15. biotite/sequence/align/multiple.cp312-win_amd64.pyd +0 -0
  16. biotite/sequence/align/multiple.pyx +1 -2
  17. biotite/sequence/align/pairwise.cp312-win_amd64.pyd +0 -0
  18. biotite/sequence/align/pairwise.pyx +2 -4
  19. biotite/sequence/align/permutation.cp312-win_amd64.pyd +0 -0
  20. biotite/sequence/align/selector.cp312-win_amd64.pyd +0 -0
  21. biotite/sequence/align/tracetable.cp312-win_amd64.pyd +0 -0
  22. biotite/sequence/codec.cp312-win_amd64.pyd +0 -0
  23. biotite/sequence/phylo/nj.cp312-win_amd64.pyd +0 -0
  24. biotite/sequence/phylo/tree.cp312-win_amd64.pyd +0 -0
  25. biotite/sequence/phylo/upgma.cp312-win_amd64.pyd +0 -0
  26. biotite/structure/basepairs.py +13 -14
  27. biotite/structure/bonds.cp312-win_amd64.pyd +0 -0
  28. biotite/structure/bonds.pyx +67 -6
  29. biotite/structure/box.py +141 -3
  30. biotite/structure/celllist.cp312-win_amd64.pyd +0 -0
  31. biotite/structure/celllist.pyx +0 -1
  32. biotite/structure/chains.py +15 -21
  33. biotite/structure/charges.cp312-win_amd64.pyd +0 -0
  34. biotite/structure/compare.py +2 -0
  35. biotite/structure/dotbracket.py +4 -4
  36. biotite/structure/graphics/rna.py +19 -16
  37. biotite/structure/hbond.py +1 -2
  38. biotite/structure/info/components.bcif +0 -0
  39. biotite/structure/io/pdb/convert.py +84 -2
  40. biotite/structure/io/pdb/file.py +94 -7
  41. biotite/structure/io/pdb/hybrid36.cp312-win_amd64.pyd +0 -0
  42. biotite/structure/io/pdbx/bcif.py +6 -3
  43. biotite/structure/io/pdbx/cif.py +5 -2
  44. biotite/structure/io/pdbx/compress.py +71 -34
  45. biotite/structure/io/pdbx/convert.py +226 -58
  46. biotite/structure/io/pdbx/encoding.cp312-win_amd64.pyd +0 -0
  47. biotite/structure/io/pdbx/encoding.pyx +39 -23
  48. biotite/structure/pseudoknots.py +6 -6
  49. biotite/structure/residues.py +10 -27
  50. biotite/structure/rings.py +118 -2
  51. biotite/structure/sasa.cp312-win_amd64.pyd +0 -0
  52. biotite/structure/sasa.pyx +28 -29
  53. biotite/structure/segments.py +55 -0
  54. biotite/structure/spacegroups.json +1567 -0
  55. biotite/structure/spacegroups.license +26 -0
  56. biotite/structure/superimpose.py +1 -191
  57. biotite/structure/transform.py +220 -1
  58. biotite/version.py +2 -2
  59. {biotite-1.2.0.dist-info → biotite-1.4.0.dist-info}/METADATA +4 -34
  60. {biotite-1.2.0.dist-info → biotite-1.4.0.dist-info}/RECORD +62 -60
  61. {biotite-1.2.0.dist-info → biotite-1.4.0.dist-info}/WHEEL +1 -1
  62. {biotite-1.2.0.dist-info → biotite-1.4.0.dist-info}/licenses/LICENSE.rst +0 -0
@@ -28,15 +28,15 @@ class RNAplotApp(LocalApp):
28
28
 
29
29
  Parameters
30
30
  ----------
31
- dot_bracket : str, optional (default: None)
31
+ dot_bracket : str, optional
32
32
  The structure in dot bracket notation.
33
- base_pairs : ndarray, shape=(n,2), optional (default: None)
33
+ base_pairs : ndarray, shape=(n,2), optional
34
34
  Each row corresponds to the positions of the bases in the
35
35
  strand. This parameter is mutually exclusive to ``dot_bracket``.
36
- length : int, optional (default: None)
36
+ length : int, optional
37
37
  The number of bases in the strand. This parameter is required if
38
38
  ``base_pairs`` is given.
39
- layout_type : RNAplotApp.Layout, optional (default: RNAplotApp.Layout.NAVIEW)
39
+ layout_type : RNAplotApp.Layout, optional
40
40
  The layout type according to the *RNAplot* documentation.
41
41
  bin_path : str, optional
42
42
  Path of the *RNAplot* binary.
@@ -176,13 +176,13 @@ class RNAplotApp(LocalApp):
176
176
 
177
177
  Parameters
178
178
  ----------
179
- dot_bracket : str, optional (default: None)
179
+ dot_bracket : str, optional
180
180
  The structure in dot bracket notation.
181
- base_pairs : ndarray, shape=(n,2), optional (default: None)
181
+ base_pairs : ndarray, shape=(n,2), optional
182
182
  Each row corresponds to the positions of the bases in the
183
183
  strand. This parameter is mutually exclusive to
184
184
  ``dot_bracket``.
185
- length : int, optional (default: None)
185
+ length : int, optional
186
186
  The number of bases in the strand. This parameter is
187
187
  required if ``base_pairs`` is given.
188
188
  layout_type : Layout
@@ -12,5 +12,9 @@ structure-related objects from *OpenMM*.
12
12
  __name__ = "biotite.interface.openmm"
13
13
  __author__ = "Patrick Kunzmann"
14
14
 
15
+ from biotite.interface.version import require_package
16
+
17
+ require_package("openmm")
18
+
15
19
  from .state import *
16
20
  from .system import *
@@ -162,6 +162,9 @@ or ``pymol_interface.cmd`` at the required places in your code.
162
162
  __name__ = "biotite.interface.pymol"
163
163
  __author__ = "Patrick Kunzmann"
164
164
 
165
+ from biotite.interface.version import require_package
166
+
167
+ require_package("pymol")
165
168
 
166
169
  from .cgo import *
167
170
  from .convert import *
@@ -388,7 +388,9 @@ class PyMOLObject:
388
388
  elif isinstance(selection, str):
389
389
  return f"%{self._name} and ({selection})"
390
390
  else:
391
- sel = self.where(np.asarray(selection))
391
+ if not isinstance(selection, slice):
392
+ selection = np.asarray(selection)
393
+ sel = self.where(selection)
392
394
  if sel == "none" and not_none:
393
395
  raise ValueError("Selection contains no atoms")
394
396
  return sel
@@ -12,4 +12,8 @@ objects.
12
12
  __name__ = "biotite.interface.rdkit"
13
13
  __author__ = "Patrick Kunzmann"
14
14
 
15
+ from biotite.interface.version import require_package
16
+
17
+ require_package("rdkit")
18
+
15
19
  from .mol import *
@@ -59,7 +59,7 @@ _STANDARD_ANNOTATIONS = frozenset(
59
59
  "charge",
60
60
  "b_factor",
61
61
  "occupancy",
62
- "label_alt_id",
62
+ "altloc_id",
63
63
  }
64
64
  )
65
65
 
@@ -202,8 +202,8 @@ def to_mol(
202
202
  rdkit_atom_res_info.SetOccupancy(atoms.occupancy[i].item())
203
203
  if "b_factor" in has_annot:
204
204
  rdkit_atom_res_info.SetTempFactor(atoms.b_factor[i].item())
205
- if "label_alt_id" in has_annot:
206
- rdkit_atom_res_info.SetAltLoc(atoms.label_alt_id[i].item())
205
+ if "altloc_id" in has_annot:
206
+ rdkit_atom_res_info.SetAltLoc(atoms.altloc_id[i].item())
207
207
  rdkit_atom.SetPDBResidueInfo(rdkit_atom_res_info)
208
208
 
209
209
  # add extra annotations
@@ -361,7 +361,7 @@ def from_mol(mol, conformer_id=None, add_hydrogen=None):
361
361
  atoms.add_annotation("charge", int)
362
362
  atoms.add_annotation("b_factor", float)
363
363
  atoms.add_annotation("occupancy", float)
364
- atoms.add_annotation("label_alt_id", str)
364
+ atoms.add_annotation("altloc_id", str)
365
365
 
366
366
  for rdkit_atom in rdkit_atoms:
367
367
  _atom_idx = rdkit_atom.GetIdx()
@@ -406,7 +406,7 @@ def from_mol(mol, conformer_id=None, add_hydrogen=None):
406
406
  atoms.res_id[_atom_idx] = residue_info.GetResidueNumber()
407
407
  atoms.ins_code[_atom_idx] = residue_info.GetInsertionCode()
408
408
  atoms.res_name[_atom_idx] = residue_info.GetResidueName()
409
- atoms.label_alt_id[_atom_idx] = residue_info.GetAltLoc()
409
+ atoms.altloc_id[_atom_idx] = residue_info.GetAltLoc()
410
410
  atoms.hetero[_atom_idx] = residue_info.GetIsHeteroAtom()
411
411
  atoms.b_factor[_atom_idx] = residue_info.GetTempFactor()
412
412
  atoms.occupancy[_atom_idx] = residue_info.GetOccupancy()
@@ -26,6 +26,29 @@ class VersionError(Exception):
26
26
  pass
27
27
 
28
28
 
29
+ def require_package(package):
30
+ """
31
+ Check if the given package is installed and raise an exception if not.
32
+
33
+ Parameters
34
+ ----------
35
+ package : str
36
+ The name of the package to be checked.
37
+
38
+ Raises
39
+ ------
40
+ ImportError
41
+ If the package is not installed.
42
+
43
+ Notes
44
+ -----
45
+ It is useful to call this function in the ``__init__.py`` of each ``interface``
46
+ subpackage, to obtain clear error messages about missing dependencies.
47
+ """
48
+ if importlib.util.find_spec(package) is None:
49
+ raise ImportError(f"'{package}' is not installed")
50
+
51
+
29
52
  def requires_version(package, version_specifier):
30
53
  """
31
54
  Declare a function variant that is compatible with a specific version range of the
@@ -76,7 +76,7 @@ def align_banded(seq1, seq2, matrix, band, gap_penalty=-10, local=False,
76
76
  If a tuple is provided, an affine gap penalty is used.
77
77
  The first integer in the tuple is the gap opening penalty,
78
78
  the second integer is the gap extension penalty.
79
- The values need to be negative. (Default: *-10*)
79
+ The values need to be negative.
80
80
  local : bool, optional
81
81
  If set to true, a local alignment is performed.
82
82
  Otherwise (default) a semi-global alignment is performed.
@@ -92,10 +92,9 @@ def align_multiple(sequences, matrix, gap_penalty=-10, terminal_penalty=True,
92
92
  penalty is used. The first integer in the tuple is the gap
93
93
  opening penalty, the second integer is the gap extension
94
94
  penalty.
95
- The values need to be negative. (Default: *-10*)
95
+ The values need to be negative.
96
96
  terminal_penalty : bool, optional
97
97
  If true, gap penalties are applied to terminal gaps.
98
- (Default: True)
99
98
  distances : ndarray, shape=(n,n)
100
99
  Pairwise distances of the sequences.
101
100
  The matrix must be symmetric and all entries must be larger
@@ -138,19 +138,17 @@ def align_optimal(seq1, seq2, matrix, gap_penalty=-10,
138
138
  If a tuple is provided, an affine gap penalty is used.
139
139
  The first integer in the tuple is the gap opening penalty,
140
140
  the second integer is the gap extension penalty.
141
- The values need to be negative. (Default: *-10*)
141
+ The values need to be negative.
142
142
  terminal_penalty : bool, optional
143
143
  If true, gap penalties are applied to terminal gaps.
144
144
  If `local` is true, this parameter has no effect.
145
- (Default: True)
146
145
  local : bool, optional
147
146
  If false, a global alignment is performed, otherwise a local
148
- alignment is performed. (Default: False)
147
+ alignment is performed.
149
148
  max_number : int, optional
150
149
  The maximum number of alignments returned.
151
150
  When the number of branches exceeds this value in the traceback
152
151
  step, no further branches are created.
153
- (Default: 1000)
154
152
 
155
153
  Returns
156
154
  -------
Binary file
@@ -638,7 +638,7 @@ def base_stacking(atom_array, min_atoms_per_base=3):
638
638
  ----------
639
639
  atom_array : AtomArray
640
640
  The :class:`AtomArray` to find stacked bases in.
641
- min_atoms_per_base : integer, optional (default: 3)
641
+ min_atoms_per_base : integer, optional
642
642
  The number of atoms a nucleotides' base must have to be
643
643
  considered a candidate for a stacking interaction.
644
644
 
@@ -783,10 +783,10 @@ def base_pairs(atom_array, min_atoms_per_base=3, unique=True):
783
783
  ----------
784
784
  atom_array : AtomArray
785
785
  The :class:`AtomArray` to find base pairs in.
786
- min_atoms_per_base : integer, optional (default: 3)
786
+ min_atoms_per_base : integer, optional
787
787
  The number of atoms a nucleotides' base must have to be
788
788
  considered a candidate for a base pair.
789
- unique : bool, optional (default: True)
789
+ unique : bool, optional
790
790
  If ``True``, each base is assumed to be only paired with one
791
791
  other base. If multiple pairings are plausible, the pairing with
792
792
  the most hydrogen bonds is selected.
@@ -1203,26 +1203,25 @@ def map_nucleotide(residue, min_atoms_per_base=3, rmsd_cutoff=0.28):
1203
1203
  If a different nucleotide is given, it is mapped to the best
1204
1204
  fitting base using the algorithm described below.
1205
1205
 
1206
- (i) The number of matching atom names with the reference bases is
1207
- counted. If the number of matching atoms with all reference
1208
- bases is less than the specified `min_atoms_per_base`
1209
- (default 3) the nucleotide cannot be mapped and ``None`` is
1206
+ (i) The number of matching atom names with the reference bases is counted.
1207
+ If the number of matching atoms with all reference bases is less than the
1208
+ specified `min_atoms_per_base` the nucleotide cannot be mapped and ``None`` is
1210
1209
  returned.
1211
1210
 
1212
- (ii) The bases with maximum number of matching atoms are selected
1213
- and superimposed with each reference. The base with lowest RMSD
1214
- is chosen. If the RMSD is more than the specified
1215
- `rmsd_cutoff` (default 0.28) the nucleotide cannot be mapped
1216
- and ``None`` is returned.
1211
+ (ii) The bases with maximum number of matching atoms are selected and superimposed
1212
+ with each reference.
1213
+ The base with lowest RMSD is chosen.
1214
+ If the RMSD is more than the specified `rmsd_cutoff`, the nucleotide cannot be
1215
+ mapped and ``None`` is returned.
1217
1216
 
1218
1217
  Parameters
1219
1218
  ----------
1220
1219
  residue : AtomArray
1221
1220
  The nucleotide to be mapped.
1222
- min_atoms_per_base : int, optional (default: 3)
1221
+ min_atoms_per_base : int, optional
1223
1222
  The number of atoms the residue must have in common with the
1224
1223
  reference.
1225
- rmsd_cutoff : float, optional (default: 0.28)
1224
+ rmsd_cutoff : float, optional
1226
1225
  The maximum RSMD that is allowed for a mapping to occur.
1227
1226
 
1228
1227
  Returns
Binary file
@@ -517,14 +517,41 @@ class BondList(Copyable):
517
517
  0 1 SINGLE
518
518
  1 2 DOUBLE
519
519
  """
520
- bond_types = self._bonds[:,2]
521
520
  for aromatic_type, non_aromatic_type in [
522
521
  (BondType.AROMATIC_SINGLE, BondType.SINGLE),
523
522
  (BondType.AROMATIC_DOUBLE, BondType.DOUBLE),
524
523
  (BondType.AROMATIC_TRIPLE, BondType.TRIPLE),
525
524
  (BondType.AROMATIC, BondType.ANY),
526
525
  ]:
527
- bond_types[bond_types == aromatic_type] = non_aromatic_type
526
+ mask = self._bonds[:, 2] == aromatic_type
527
+ self._bonds[mask, 2] = non_aromatic_type
528
+
529
+ def remove_kekulization(self):
530
+ """
531
+ Remove the bond order information from aromatic bonds, i.e. convert all
532
+ aromatic bonds to :attr:`BondType.ANY`.
533
+
534
+ Examples
535
+ --------
536
+
537
+ >>> bond_list = BondList(3)
538
+ >>> bond_list.add_bond(0, 1, BondType.AROMATIC_SINGLE)
539
+ >>> bond_list.add_bond(1, 2, BondType.AROMATIC_DOUBLE)
540
+ >>> bond_list.remove_kekulization()
541
+ >>> for i, j, bond_type in bond_list.as_array():
542
+ ... print(i, j, BondType(bond_type).name)
543
+ 0 1 AROMATIC
544
+ 1 2 AROMATIC
545
+ """
546
+ kekulized_mask = np.isin(
547
+ self._bonds[:, 2],
548
+ (
549
+ BondType.AROMATIC_SINGLE,
550
+ BondType.AROMATIC_DOUBLE,
551
+ BondType.AROMATIC_TRIPLE,
552
+ ),
553
+ )
554
+ self._bonds[kekulized_mask, 2] = BondType.AROMATIC
528
555
 
529
556
  def remove_bond_order(self):
530
557
  """
@@ -532,6 +559,41 @@ class BondList(Copyable):
532
559
  """
533
560
  self._bonds[:,2] = BondType.ANY
534
561
 
562
+ def convert_bond_type(self, original_bond_type, new_bond_type):
563
+ """
564
+ convert_bond_type(original_bond_type, new_bond_type)
565
+
566
+ Convert all occurences of a given bond type into another bond type.
567
+
568
+ Parameters
569
+ ----------
570
+ original_bond_type : BondType or int
571
+ The bond type to convert.
572
+ new_bond_type : BondType or int
573
+ The new bond type.
574
+
575
+ Examples
576
+ --------
577
+
578
+ >>> bond_list = BondList(4)
579
+ >>> bond_list.add_bond(0, 1, BondType.DOUBLE)
580
+ >>> bond_list.add_bond(1, 2, BondType.COORDINATION)
581
+ >>> bond_list.add_bond(2, 3, BondType.COORDINATION)
582
+ >>> for i, j, bond_type in bond_list.as_array():
583
+ ... print(i, j, BondType(bond_type).name)
584
+ 0 1 DOUBLE
585
+ 1 2 COORDINATION
586
+ 2 3 COORDINATION
587
+ >>> bond_list.convert_bond_type(BondType.COORDINATION, BondType.SINGLE)
588
+ >>> for i, j, bond_type in bond_list.as_array():
589
+ ... print(i, j, BondType(bond_type).name)
590
+ 0 1 DOUBLE
591
+ 1 2 SINGLE
592
+ 2 3 SINGLE
593
+ """
594
+ mask = self._bonds[:, 2] == original_bond_type
595
+ self._bonds[mask, 2] = new_bond_type
596
+
535
597
  def get_atom_count(self):
536
598
  """
537
599
  get_atom_count()
@@ -1437,9 +1499,8 @@ _DEFAULT_DISTANCE_RANGE = {
1437
1499
  def connect_via_distances(atoms, dict distance_range=None, bint inter_residue=True,
1438
1500
  default_bond_type=BondType.ANY, bint periodic=False):
1439
1501
  """
1440
- connect_via_distances(atoms, distance_range=None, atom_mask=None,
1441
- inter_residue=True, default_bond_type=BondType.ANY,
1442
- periodic=False)
1502
+ connect_via_distances(atoms, distance_range=None, inter_residue=True,
1503
+ default_bond_type=BondType.ANY, periodic=False)
1443
1504
 
1444
1505
  Create a :class:`BondList` for a given atom array, based on
1445
1506
  pairwise atom distances.
@@ -1589,7 +1650,7 @@ def connect_via_distances(atoms, dict distance_range=None, bint inter_residue=Tr
1589
1650
  def connect_via_residue_names(atoms, bint inter_residue=True,
1590
1651
  dict custom_bond_dict=None):
1591
1652
  """
1592
- connect_via_residue_names(atoms, atom_mask=None, inter_residue=True)
1653
+ connect_via_residue_names(atoms, inter_residue=True, custom_bond_dict=None)
1593
1654
 
1594
1655
  Create a :class:`BondList` for a given atom array (stack), based on
1595
1656
  the deposited bonds for each residue in the RCSB ``components.cif``
biotite/structure/box.py CHANGED
@@ -4,12 +4,13 @@
4
4
 
5
5
  """
6
6
  Functions related to working with the simulation box or unit cell
7
- of a structure
7
+ of a structure.
8
8
  """
9
9
 
10
10
  __name__ = "biotite.structure"
11
11
  __author__ = "Patrick Kunzmann"
12
12
  __all__ = [
13
+ "space_group_transforms",
13
14
  "vectors_from_unitcell",
14
15
  "unitcell_from_vectors",
15
16
  "box_volume",
@@ -23,16 +24,127 @@ __all__ = [
23
24
  "is_orthogonal",
24
25
  ]
25
26
 
27
+ import functools
28
+ import json
26
29
  from numbers import Integral
30
+ from pathlib import Path
27
31
  import numpy as np
28
32
  import numpy.linalg as linalg
29
33
  from biotite.structure.atoms import repeat
30
34
  from biotite.structure.chains import get_chain_masks, get_chain_starts
31
35
  from biotite.structure.error import BadStructureError
32
36
  from biotite.structure.molecules import get_molecule_masks
37
+ from biotite.structure.transform import AffineTransformation
33
38
  from biotite.structure.util import vector_dot
34
39
 
35
40
 
41
+ def space_group_transforms(space_group):
42
+ """
43
+ Get the coordinate transformations for a given space group.
44
+
45
+ Applying each transformation to a structure (in fractional coordinates) reproduces
46
+ the entire unit cell.
47
+
48
+ Parameters
49
+ ----------
50
+ space_group : str or int
51
+ The space group name (full *Hermann-Mauguin* symbol) or
52
+ *International Table*'s number.
53
+
54
+ Returns
55
+ -------
56
+ transformations : list of AffineTransformation
57
+ The transformations that creates the symmetric copies of a structure in a unit
58
+ cell of the given space group.
59
+ Note that the transformations need to be applied to coordinates in fractions
60
+ of the unit cell and also return fractional coordinates, when applied.
61
+
62
+ See Also
63
+ --------
64
+ coord_to_fraction : Used to convert to fractional coordinates.
65
+ fraction_to_coord : Used to convert back to Cartesian coordinates.
66
+
67
+ Examples
68
+ --------
69
+
70
+ >>> transforms = space_group_transforms("P 21 21 21")
71
+ >>> for transform in transforms:
72
+ ... print(transform.rotation)
73
+ ... print(transform.target_translation)
74
+ ... print()
75
+ [[[1. 0. 0.]
76
+ [0. 1. 0.]
77
+ [0. 0. 1.]]]
78
+ [[0. 0. 0.]]
79
+ <BLANKLINE>
80
+ [[[-1. 0. 0.]
81
+ [ 0. -1. 0.]
82
+ [ 0. 0. 1.]]]
83
+ [[0.5 0.0 0.5]]
84
+ <BLANKLINE>
85
+ [[[-1. 0. 0.]
86
+ [ 0. 1. 0.]
87
+ [ 0. 0. -1.]]]
88
+ [[0.0 0.5 0.5]]
89
+ <BLANKLINE>
90
+ [[[ 1. 0. 0.]
91
+ [ 0. -1. 0.]
92
+ [ 0. 0. -1.]]]
93
+ [[0.5 0.5 0.0]]
94
+ <BLANKLINE>
95
+
96
+ Reproduce the unit cell for some coordinates (in this case only one atom).
97
+
98
+ >>> asym_coord = np.array([[1.0, 2.0, 3.0]])
99
+ >>> box = np.eye(3) * 10
100
+ >>> transforms = space_group_transforms("P 21 21 21")
101
+ >>> # Apply the transformations to fractional coordinates of the asymmetric unit
102
+ >>> unit_cell = np.concatenate(
103
+ ... [
104
+ ... fraction_to_coord(transform.apply(coord_to_fraction(asym_coord, box)), box)
105
+ ... for transform in transforms
106
+ ... ]
107
+ ... )
108
+ >>> print(unit_cell)
109
+ [[ 1. 2. 3.]
110
+ [ 4. -2. 8.]
111
+ [-1. 7. 2.]
112
+ [ 6. 3. -3.]]
113
+ """
114
+ transformation_data = _get_transformation_data()
115
+
116
+ if isinstance(space_group, str):
117
+ try:
118
+ space_group_index = transformation_data["group_names"][space_group]
119
+ except KeyError:
120
+ raise ValueError(f"Space group '{space_group}' does not exist")
121
+ else:
122
+ try:
123
+ space_group_index = transformation_data["group_numbers"][str(space_group)]
124
+ except KeyError:
125
+ raise ValueError(f"Space group number {space_group} does not exist")
126
+
127
+ space_group = transformation_data["space_groups"][space_group_index]
128
+ transformations = []
129
+ for transformation_index in space_group:
130
+ matrix = np.zeros((3, 3), dtype=np.float32)
131
+ translation = np.zeros(3, dtype=np.float32)
132
+ for i, part_index in enumerate(
133
+ transformation_data["transformations"][transformation_index]
134
+ ):
135
+ part = transformation_data["transformation_parts"][part_index]
136
+ matrix[i, :] = part[:3]
137
+ translation[i] = part[3]
138
+ transformations.append(
139
+ AffineTransformation(
140
+ center_translation=np.zeros(3, dtype=np.float32),
141
+ rotation=matrix,
142
+ target_translation=translation,
143
+ )
144
+ )
145
+ return transformations
146
+
147
+
36
148
  def vectors_from_unitcell(len_a, len_b, len_c, alpha, beta, gamma):
37
149
  """
38
150
  Calculate the three vectors spanning a box from the unit cell
@@ -160,6 +272,8 @@ def repeat_box(atoms, amount=1):
160
272
  The repeated atoms.
161
273
  Includes the original atoms (central box) in the beginning of
162
274
  the atom array (stack).
275
+ If the input contains the ``sym_id`` annotation, the IDs are continued in the
276
+ repeated atoms, i.e. they do not start at 0 again.
163
277
  indices : ndarray, dtype=int, shape=(n,3)
164
278
  Indices to the atoms in the original atom array (stack).
165
279
  Equal to
@@ -234,11 +348,20 @@ def repeat_box(atoms, amount=1):
234
348
  >>> print(indices)
235
349
  [0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
236
350
  1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1]
351
+
352
+ The ``sym_id`` is continued in the repeated atoms.
353
+
354
+ >>> array.set_annotation("sym_id", np.array([0, 0]))
355
+ >>> repeated, indices = repeat_box(array)
356
+ >>> print(repeated.sym_id)
357
+ [ 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10 11 11
358
+ 12 12 13 13 14 14 15 15 16 16 17 17 18 18 19 19 20 20 21 21 22 22 23 23
359
+ 24 24 25 25 26 26]
237
360
  """
238
361
  if atoms.box is None:
239
362
  raise BadStructureError("Structure has no box")
240
363
 
241
- repeat_coord, indices = repeat_box_coord(atoms.coord, atoms.box)
364
+ repeat_coord, indices = repeat_box_coord(atoms.coord, atoms.box, amount)
242
365
  # Unroll repeated coordinates for input to 'repeat()'
243
366
  if repeat_coord.ndim == 2:
244
367
  repeat_coord = repeat_coord.reshape(-1, atoms.array_length(), 3)
@@ -247,7 +370,16 @@ def repeat_box(atoms, amount=1):
247
370
  atoms.stack_depth(), -1, atoms.array_length(), 3
248
371
  )
249
372
  repeat_coord = np.swapaxes(repeat_coord, 0, 1)
250
- return repeat(atoms, repeat_coord), indices
373
+
374
+ repeated_atoms = repeat(atoms, repeat_coord)
375
+ if "sym_id" in atoms.get_annotation_categories():
376
+ max_sym_id = np.max(atoms.sym_id)
377
+ # for the first repeat, (max_sym_id + 1) is added,
378
+ # for the second repeat 2*(max_sym_id + 1) etc.
379
+ repeated_atoms.sym_id += (max_sym_id + 1) * (
380
+ np.arange(repeated_atoms.array_length()) // atoms.array_length()
381
+ )
382
+ return repeated_atoms, indices
251
383
 
252
384
 
253
385
  def repeat_box_coord(coord, box, amount=1):
@@ -584,3 +716,9 @@ def is_orthogonal(box):
584
716
  & (np.abs(vector_dot(box[..., 0, :], box[..., 2, :])) < tol)
585
717
  & (np.abs(vector_dot(box[..., 1, :], box[..., 2, :])) < tol)
586
718
  )
719
+
720
+
721
+ @functools.cache
722
+ def _get_transformation_data():
723
+ with open(Path(__file__).parent / "spacegroups.json") as file:
724
+ return json.load(file)
@@ -55,7 +55,6 @@ cdef class CellList:
55
55
  periodic : bool, optional
56
56
  If true, the cell list considers periodic copies of atoms.
57
57
  The periodicity is based on the `box` attribute of `atom_array`.
58
- (Default: False)
59
58
  box : ndarray, dtype=float, shape=(3,3), optional
60
59
  If provided, the periodicity is based on this parameter instead
61
60
  of the :attr:`box` attribute of `atom_array`.