From 7f85d289d9365732725c2712c52096cb5656b787 Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:20:56 -0500 Subject: [PATCH 01/12] Standardize cif example in parsnip module --- parsnip/parsnip.py | 47 +++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index 616a1fb..c59421e 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -19,28 +19,45 @@ .. code-block:: text - # Key-value pairs describing the unit cell: - _cell_length_a 5.40 - _cell_length_b 3.43 - _cell_length_c 5.08 + # A header describing this portion of the file + data_cif_Cu-FCC + + # Several key-value pairs + _journal_year 1999 + _journal_page_first 0 + _journal_page_last 123 + + _chemical_name_mineral 'Copper FCC' + _chemical_formula_sum 'Cu' + + # Key-value pairs describing the unit cell (Å and °) + _cell_length_a 3.6 + _cell_length_b 3.6 + _cell_length_c 3.6 _cell_angle_alpha 90.0 - _cell_angle_beta 132.3 + _cell_angle_beta 90.0 _cell_angle_gamma 90.0 - # A table with two columns and eight rows: + # A table with 6 columns and one row + loop_ + _atom_site_label + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_type_symbol + _atom_site_Wyckoff_label + Cu1 0.0000000000 0.0000000000 0.0000000000 Cu a + + _symmetry_space_group_name_H-M 'Fm-3m' # One more key-value pair + + # A table with two columns and four rows: loop_ _symmetry_equiv_pos_site_id _symmetry_equiv_pos_as_xyz 1 x,y,z - 2 -x,y,-z - 3 -x,-y,-z - 4 x,-y,z - 5 x+1/2,y+1/2,z - 6 -x+1/2,y+1/2,-z - 7 -x+1/2,-y+1/2,-z - 8 x+1/2,-y+1/2,z - - _symmetry_space_group_name_H-M 'C2 / m' # One more key-value pair + 96 z,y+1/2,x+1/2 + 118 z+1/2,-y,x+1/2 + 192 z+1/2,y+1/2,x .. _key: https://www.iucr.org/resources/cif/spec/version1.1/cifsyntax#definitions From 71a1da673fb39bb01e009324b013e441f44e407f Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:21:48 -0500 Subject: [PATCH 02/12] Rename table_labels to loop_labels --- parsnip/parsnip.py | 2 +- tests/test_table_reader.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index c59421e..5c6c938 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -184,7 +184,7 @@ def loops(self): return self._loops @property - def table_labels(self): + def loop_labels(self): """A list of column labels for each data array. This property is equivalent to :code:`[arr.dtype.names for arr in self.loops]`. diff --git a/tests/test_table_reader.py b/tests/test_table_reader.py index b8e84de..35df9ef 100644 --- a/tests/test_table_reader.py +++ b/tests/test_table_reader.py @@ -26,7 +26,7 @@ def test_reads_all_keys(cif_data): loop_keys = [*flatten(pycif.loops.values())] all_keys = [key for key in pycif.true_case.values() if key.lower() in loop_keys] - found_labels = [*flatten(cif_data.file.table_labels)] + found_labels = [*flatten(cif_data.file.loop_labels)] for key in all_keys: assert key in found_labels, f"Missing label: {found_labels}" @@ -50,7 +50,7 @@ def test_read_atom_sites(cif_data): parsnip_data = cif_data.file.get_from_loops(cif_data.atom_site_keys) gemmi_data = _gemmi_read_table(cif_data.filename, cif_data.atom_site_keys) np.testing.assert_array_equal(parsnip_data, gemmi_data) - assert (key in cif_data.file.table_labels for key in cif_data.atom_site_keys) + assert (key in cif_data.file.loop_labels for key in cif_data.atom_site_keys) if not any(s in cif_data.filename for s in ["CCDC", "PDB", "AMCSD", "zeolite"]): import sys From 3a6962a3c129427396e878e530b9cb97d4790158 Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:22:49 -0500 Subject: [PATCH 03/12] Move parameterless read methods to properties --- parsnip/parsnip.py | 125 ++++++++++++++++++++-------------------- tests/test_unitcells.py | 4 +- 2 files changed, 66 insertions(+), 63 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index 5c6c938..3545fcd 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -389,39 +389,6 @@ def angle_is_invalid(x: float): return tuple(float(v) for v in cell_data) # Return as base python types - def read_symmetry_operations(self): - r"""Extract the symmetry operations from a CIF file. - - Returns - ------- - :math:`(N,)` :class:`numpy.ndarray[str]`: - An array of strings containing the symmetry operations in a - `parsable algebraic form`_. - - .. _`parsable algebraic form`: https://www.iucr.org/__data/iucr/cifdic_html/1/cif_core.dic/Ispace_group_symop_operation_xyz.html - """ - symmetry_keys = ( - "_symmetry_equiv_pos_as_xyz", - "_space_group_symop_operation_xyz", - ) - - # Only one key is valid in each standard, so we only ever get one match. - return self.get_from_loops(symmetry_keys) - - def read_wyckoff_positions(self): - r"""Extract symmetry-irreducible, fractional x,y,z coordinates from a CIF file. - - Returns - ------- - :math:`(N, 3)` :class:`numpy.ndarray[float]`: - Symmetry-irreducible positions of atoms in `fractional coordinates`_. - - .. _`fractional coordinates`: https://www.iucr.org/__data/iucr/cifdic_html/1/cif_core.dic/Iatom_site_fract_.html - """ - xyz_keys = ("_atom_site_fract_x", "_atom_site_fract_y", "_atom_site_fract_z") - - return cast_array_to_float(arr=self.get_from_loops(xyz_keys), dtype=float) - def build_unit_cell( self, fractional: bool = True, @@ -468,15 +435,14 @@ def build_unit_cell( ValueError If the stored data cannot form a valid box. """ - fractional_positions = self.read_wyckoff_positions() + fractional_positions = self.wyckoff_positions # Read the cell params and convert to a matrix of basis vectors cell = self.read_cell_params(degrees=False, mmcif=False) cell_matrix = _matrix_from_lengths_and_angles(*cell) - symops = self.read_symmetry_operations() symops_str = np.array2string( - symops, + self.symops, separator=",", # Place a comma after each line in the array for eval threshold=np.inf, # Ensure that every line is included in the string floatmode="unique", # Ensures strings can uniquely represent each float @@ -516,31 +482,6 @@ def build_unit_cell( pos[unique_indices] if fractional else real_space_positions[unique_indices] ) - @property - def cast_values(self): - """Bool : Whether to cast "number-like" values to ints & floats. - - .. note:: - - When set to `True` after construction, the values are modified in-place. - This action cannot be reversed. - """ - return self._cast_values - - @cast_values.setter - def cast_values(self, cast: bool): - if cast: - self._pairs = { - k: _try_cast_to_numeric(_strip_quotes(v)) - for (k, v) in self.pairs.items() - } - else: - warnings.warn( - "Setting cast_values True->False has no effect on stored data.", - category=ParseWarning, - stacklevel=2, - ) - self._cast_values = cast @property def box(self): @@ -579,6 +520,68 @@ def box(self): *self.read_cell_params(degrees=False, mmcif=False) ) + @property + def symops(self): + r"""Extract the symmetry operations from a CIF file. + + Returns + ------- + :math:`(N,)` :class:`numpy.ndarray[str]`: + An array of strings containing the symmetry operations in a + `parsable algebraic form`_. + + .. _`parsable algebraic form`: https://www.iucr.org/__data/iucr/cifdic_html/1/cif_core.dic/Ispace_group_symop_operation_xyz.html + """ + symmetry_keys = ( + "_symmetry_equiv_pos_as_xyz", + "_space_group_symop_operation_xyz", + ) + + # Only one key is valid in each standard, so we only ever get one match. + return self.get_from_loops(symmetry_keys) + + @property + def wyckoff_positions(self): + r"""Extract symmetry-irreducible, fractional x,y,z coordinates from a CIF file. + + Returns + ------- + :math:`(N, 3)` :class:`numpy.ndarray[float]`: + Symmetry-irreducible positions of atoms in `fractional coordinates`_. + + .. _`fractional coordinates`: https://www.iucr.org/__data/iucr/cifdic_html/1/cif_core.dic/Iatom_site_fract_.html + """ + xyz_keys = ("_atom_site_fract_x", "_atom_site_fract_y", "_atom_site_fract_z") + + return cast_array_to_float(arr=self.get_from_loops(xyz_keys), dtype=float) + + + @property + def cast_values(self): + """Bool : Whether to cast "number-like" values to ints & floats. + + .. note:: + + When set to `True` after construction, the values are modified in-place. + This action cannot be reversed. + """ + return self._cast_values + + @cast_values.setter + def cast_values(self, cast: bool): + if cast: + self._pairs = { + k: _try_cast_to_numeric(_strip_quotes(v)) + for (k, v) in self.pairs.items() + } + else: + warnings.warn( + "Setting cast_values True->False has no effect on stored data.", + category=ParseWarning, + stacklevel=2, + ) + self._cast_values = cast + @classmethod def structured_to_unstructured(cls, arr: np.ndarray): """Convert a structured (column-labeled) array to a standard unstructured array. diff --git a/tests/test_unitcells.py b/tests/test_unitcells.py index 27f103a..e689d93 100644 --- a/tests/test_unitcells.py +++ b/tests/test_unitcells.py @@ -24,7 +24,7 @@ def test_read_wyckoff_positions(cif_data): if "PDB_4INS_head.cif" in cif_data.filename: return keys = ("_atom_site_fract_x", "_atom_site_fract_y", "_atom_site_fract_z") - parsnip_data = cif_data.file.read_wyckoff_positions() + parsnip_data = cif_data.file.wyckoff_positions gemmi_data = _gemmi_read_table(cif_data.filename, keys) gemmi_data = [[cif.as_number(val) for val in row] for row in gemmi_data] np.testing.assert_array_equal(parsnip_data, gemmi_data) @@ -45,7 +45,7 @@ def test_read_symmetry_operations(cif_data): if "PDB_4INS_head.cif" in cif_data.filename: return # Excerpt of PDB file does not contain symmetry information - parsnip_data = cif_data.file.read_symmetry_operations() + parsnip_data = cif_data.file.symops gemmi_data = _gemmi_read_table(filename=cif_data.filename, keys=cif_data.symop_keys) np.testing.assert_array_equal(parsnip_data, gemmi_data) From 5f5d7301cf17fd37e79a25c3e966a0ffe25fa0e6 Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:24:32 -0500 Subject: [PATCH 04/12] Generalize getitem --- parsnip/parsnip.py | 49 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index 3545fcd..12f243e 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -168,7 +168,7 @@ def pairs(self): @property def loops(self): - """A list of data tables (:code:``loop_``'s) extracted from the file. + r"""A list of data tables (`loop_`'s) extracted from the file. These are stored as `numpy structured arrays`_, which can be indexed by column labels. See the :attr:`~.structured_to_unstructured` helper function below for @@ -284,9 +284,48 @@ def get_from_loops(self, index: ArrayLike): table[matches], copy=True, casting="safe" ).squeeze(axis=1) ) - return result if len(result) != 1 else result[0] + return (result or None) if len(result) != 1 else result[0] def __getitem__(self, index: str | Iterable[str]): + """Return an item or list of items from :meth:`~.pairs` and :meth:`~.loops`. + + This getter searches the entire CIF state to identify the input keys, returning + `None` if the key does not match any data. Matching columns from `loop` tables + are returned as 1D arrays. + + .. tip:: + + This method of accessing data is recommended for most uses, as it best + ensures data is returned where possible. + + Example + ------- + Indexing the class with a single key: + + >>> cif["_journal_year"] + '1999' + >>> cif["_atom_site_label"] + array([['Cu1']], dtype='>> cif[["_journal_year", "_chemical_name_mineral", "_symmetry_equiv_pos_as_xyz"]] + ['1999', + "'Copper FCC'", + array([['x,y,z'], + ['z,y+1/2,x+1/2'], + ['z+1/2,-y,x+1/2'], + ['z+1/2,y+1/2,x']], dtype='>> cif["_journal_year"] + >>> cif.get_from_pairs("_journal_year") '1999' Indexing with a list of keys: - >>> cif[["_journal_year", "_journal_page_first", "_journal_page_last"]] - ['1999', '0', '123'] + >>> cif.get_from_pairs(["_journal_year", "_journal_page_first"]) + ['1999', '0'] Parameters ---------- From c9fd5a6c2c591af2185e5b1ddd1266bae3cd1a65 Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:28:01 -0500 Subject: [PATCH 05/12] Clarify tip for getitem --- parsnip/parsnip.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index 12f243e..a3b1bda 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -295,8 +295,9 @@ def __getitem__(self, index: str | Iterable[str]): .. tip:: - This method of accessing data is recommended for most uses, as it best - ensures data is returned where possible. + This method of accessing data is recommended for most uses, as it ensures + data is returned wherever possible. :meth:`~.get_from_loops` may be useful + when multi-column slices of an array are needed. Example ------- From 2cff3a9124b4d8834cd9956b86c0ca96b27678c3 Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:30:06 -0500 Subject: [PATCH 06/12] Lint parsnip.py --- parsnip/parsnip.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index a3b1bda..d1e0d43 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -310,9 +310,8 @@ def __getitem__(self, index: str | Iterable[str]): Indexing with a list of keys: - >>> cif[["_journal_year", "_chemical_name_mineral", "_symmetry_equiv_pos_as_xyz"]] - ['1999', - "'Copper FCC'", + >>> cif[["_chemical_name_mineral", "_symmetry_equiv_pos_as_xyz"]] + ["'Copper FCC'", array([['x,y,z'], ['z,y+1/2,x+1/2'], ['z+1/2,-y,x+1/2'], @@ -325,7 +324,6 @@ def __getitem__(self, index: str | Iterable[str]): output.append(pairs_match if pairs_match is not None else loops_match) return output[0] if len(output) == 1 else output - def get_from_pairs(self, index: str | Iterable[str]): """Return an item from the dictionary of key-value pairs. @@ -522,7 +520,6 @@ def build_unit_cell( pos[unique_indices] if fractional else real_space_positions[unique_indices] ) - @property def box(self): """Read the unit cell as a `freud`_ or HOOMD `box-like`_ object. @@ -595,7 +592,6 @@ def wyckoff_positions(self): return cast_array_to_float(arr=self.get_from_loops(xyz_keys), dtype=float) - @property def cast_values(self): """Bool : Whether to cast "number-like" values to ints & floats. From e7f2420e58abe09eb8d376c88969bc245bea5682 Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:33:25 -0500 Subject: [PATCH 07/12] Add pathlib to type hints --- parsnip/parsnip.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index d1e0d43..efeb893 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -70,6 +70,7 @@ import re import warnings from collections.abc import Iterable +from pathlib import Path from typing import ClassVar import numpy as np @@ -129,14 +130,14 @@ class CifFile: Parameters ---------- - fn : str - Name of the file to be opened. + fn : str | Path + Path to the file to be opened. cast_values : bool, optional Whether to convert string numerics to integers and float. Default value = ``False`` """ - def __init__(self, fn: str, cast_values: bool = False): + def __init__(self, fn: str | Path, cast_values: bool = False): """Create a CifFile object from a filename. On construction, the entire file is parsed into key-value pairs and data loops. From be498c71a34866da365c26f75f25fdb489b05648 Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:34:29 -0500 Subject: [PATCH 08/12] Move loop_labels --- parsnip/parsnip.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index efeb893..bad73fb 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -184,19 +184,6 @@ def loops(self): """ return self._loops - @property - def loop_labels(self): - """A list of column labels for each data array. - - This property is equivalent to :code:`[arr.dtype.names for arr in self.loops]`. - - Returns - ------- - list[list[str]]: - Column labels for :attr:`~.loops`, stored as a nested list of strings. - """ - return [arr.dtype.names for arr in self.loops] - def get_from_loops(self, index: ArrayLike): """Return a column or columns from the matching table in :attr:`~.loops`. @@ -558,6 +545,19 @@ def box(self): *self.read_cell_params(degrees=False, mmcif=False) ) + @property + def loop_labels(self): + """A list of column labels for each data array. + + This property is equivalent to :code:`[arr.dtype.names for arr in self.loops]`. + + Returns + ------- + list[list[str]]: + Column labels for :attr:`~.loops`, stored as a nested list of strings. + """ + return [arr.dtype.names for arr in self.loops] + @property def symops(self): r"""Extract the symmetry operations from a CIF file. From 7883232bf4e4175e4b76194a088c7134ac6cec4b Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:35:36 -0500 Subject: [PATCH 09/12] Move getitem to front --- parsnip/parsnip.py | 76 +++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index bad73fb..a1a1a92 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -184,6 +184,44 @@ def loops(self): """ return self._loops + def __getitem__(self, index: str | Iterable[str]): + """Return an item or list of items from :meth:`~.pairs` and :meth:`~.loops`. + + This getter searches the entire CIF state to identify the input keys, returning + `None` if the key does not match any data. Matching columns from `loop` tables + are returned as 1D arrays. + + .. tip:: + + This method of accessing data is recommended for most uses, as it ensures + data is returned wherever possible. :meth:`~.get_from_loops` may be useful + when multi-column slices of an array are needed. + + Example + ------- + Indexing the class with a single key: + + >>> cif["_journal_year"] + '1999' + >>> cif["_atom_site_label"] + array([['Cu1']], dtype='>> cif[["_chemical_name_mineral", "_symmetry_equiv_pos_as_xyz"]] + ["'Copper FCC'", + array([['x,y,z'], + ['z,y+1/2,x+1/2'], + ['z+1/2,-y,x+1/2'], + ['z+1/2,y+1/2,x']], dtype='>> cif["_journal_year"] - '1999' - >>> cif["_atom_site_label"] - array([['Cu1']], dtype='>> cif[["_chemical_name_mineral", "_symmetry_equiv_pos_as_xyz"]] - ["'Copper FCC'", - array([['x,y,z'], - ['z,y+1/2,x+1/2'], - ['z+1/2,-y,x+1/2'], - ['z+1/2,y+1/2,x']], dtype=' Date: Fri, 17 Jan 2025 22:36:35 -0500 Subject: [PATCH 10/12] Move pairs before loops --- parsnip/parsnip.py | 72 +++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index a1a1a92..bba2d73 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -222,6 +222,42 @@ def __getitem__(self, index: str | Iterable[str]): output.append(pairs_match if pairs_match is not None else loops_match) return output[0] if len(output) == 1 else output + def get_from_pairs(self, index: str | Iterable[str]): + """Return an item from the dictionary of key-value pairs. + + Indexing with a string returns the value from the :meth:`~.pairs` dict. Indexing + with an Iterable of strings returns a list of values, with `None` as a + placeholder for keys that did not match any data. + + Example + ------- + Indexing the class with a single key: + + >>> cif.get_from_pairs("_journal_year") + '1999' + + Indexing with a list of keys: + + >>> cif.get_from_pairs(["_journal_year", "_journal_page_first"]) + ['1999', '0'] + + Parameters + ---------- + index: str | Iterable[str] + An item key or list of keys. + + Returns + ------- + list[str|int|float] : + A list of data elements corresponding to the input key or keys. If the + resulting list would have length 1, the item is returned directly + instead. + """ + if isinstance(index, Iterable) and not isinstance(index, str): + return [self.pairs.get(k, None) for k in index] + + return self.pairs.get(index, None) + def get_from_loops(self, index: ArrayLike): """Return a column or columns from the matching table in :attr:`~.loops`. @@ -312,42 +348,6 @@ def get_from_loops(self, index: ArrayLike): ) return (result or None) if len(result) != 1 else result[0] - def get_from_pairs(self, index: str | Iterable[str]): - """Return an item from the dictionary of key-value pairs. - - Indexing with a string returns the value from the :meth:`~.pairs` dict. Indexing - with an Iterable of strings returns a list of values, with `None` as a - placeholder for keys that did not match any data. - - Example - ------- - Indexing the class with a single key: - - >>> cif.get_from_pairs("_journal_year") - '1999' - - Indexing with a list of keys: - - >>> cif.get_from_pairs(["_journal_year", "_journal_page_first"]) - ['1999', '0'] - - Parameters - ---------- - index: str | Iterable[str] - An item key or list of keys. - - Returns - ------- - list[str|int|float] : - A list of data elements corresponding to the input key or keys. If the - resulting list would have length 1, the item is returned directly - instead. - """ - if isinstance(index, Iterable) and not isinstance(index, str): - return [self.pairs.get(k, None) for k in index] - - return self.pairs.get(index, None) - def read_cell_params(self, degrees: bool = True, mmcif: bool = False): r"""Read the `unit cell parameters`_ (lengths and angles) from a CIF file. From 5275ac15bde8ad5a03a5d8e9c981aea3a0cc3ae9 Mon Sep 17 00:00:00 2001 From: janbridley Date: Fri, 17 Jan 2025 22:41:36 -0500 Subject: [PATCH 11/12] Expand on docs --- parsnip/parsnip.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index bba2d73..b08c6a5 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -419,7 +419,6 @@ def build_unit_cell( self, fractional: bool = True, n_decimal_places: int = 4, - wrap_coords: bool = True, # TODO: docs verbose: bool = False, ): """Reconstruct atomic positions from Wyckoff sites and symmetry operations. @@ -479,8 +478,7 @@ def build_unit_cell( ] pos = np.vstack(all_frac_positions) - if wrap_coords: - pos %= 1 # Wrap particles into the box + pos %= 1 # Wrap particles into the box # Filter unique points. This takes some time but makes the method faster overall _, unique_indices, unique_counts = np.unique( @@ -562,6 +560,14 @@ def loop_labels(self): def symops(self): r"""Extract the symmetry operations from a CIF file. + Example + ------- + >>> cif.symops + array([['x,y,z'], + ['z,y+1/2,x+1/2'], + ['z+1/2,-y,x+1/2'], + ['z+1/2,y+1/2,x']], dtype=' Date: Sat, 18 Jan 2025 11:03:09 -0500 Subject: [PATCH 12/12] Additional test for getitem --- tests/test_key_reader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_key_reader.py b/tests/test_key_reader.py index 09b30d6..9785d37 100644 --- a/tests/test_key_reader.py +++ b/tests/test_key_reader.py @@ -30,6 +30,7 @@ def test_read_key_value_pairs(cif_data): parsnip_data = cif_data.file[all_keys] for i, value in enumerate(parsnip_data): assert cif_data.file[all_keys[i]] == value + assert cif_data.file[all_keys[i]] == cif_data.file.get_from_pairs(all_keys[i]) gemmi_data = _gemmi_read_keys(cif_data.filename, keys=all_keys, as_number=False) np.testing.assert_equal(parsnip_data, gemmi_data, verbose=True) @@ -38,6 +39,7 @@ def test_read_key_value_pairs(cif_data): @random_keys_mark(n_samples=20) def test_read_key_value_pairs_random(cif_data, keys): parsnip_data = cif_data.file[keys] + np.testing.assert_equal(parsnip_data, cif_data.file.get_from_pairs(keys)) for i, value in enumerate(parsnip_data): assert cif_data.file.pairs.get(keys[i], None) == value gemmi_data = _gemmi_read_keys(cif_data.filename, keys=keys, as_number=False)