diff --git a/.github/requirements-3.10.txt b/.github/requirements-3.10.txt index a85a1736..986ca9e2 100644 --- a/.github/requirements-3.10.txt +++ b/.github/requirements-3.10.txt @@ -18,7 +18,7 @@ kiwisolver==1.4.8 # via matplotlib matplotlib==3.10.0 # via ase -more-itertools==10.5.0 +more-itertools==10.6.0 # via parsnip (pyproject.toml) numpy==2.2.1 # via @@ -32,7 +32,8 @@ packaging==24.2 # via # matplotlib # pytest -pillow==11.0.0 + # pytest-doctestplus +pillow==11.1.0 # via matplotlib pluggy==1.5.0 # via pytest @@ -43,10 +44,14 @@ pycifrw==4.4.6 pyparsing==3.2.1 # via matplotlib pytest==8.3.4 + # via + # -r tests/requirements.in + # pytest-doctestplus +pytest-doctestplus==1.3.0 # via -r tests/requirements.in python-dateutil==2.9.0.post0 # via matplotlib -scipy==1.14.1 +scipy==1.15.1 # via ase six==1.17.0 # via python-dateutil diff --git a/.github/requirements-3.11.txt b/.github/requirements-3.11.txt index 27d68091..4d1b928d 100644 --- a/.github/requirements-3.11.txt +++ b/.github/requirements-3.11.txt @@ -16,7 +16,7 @@ kiwisolver==1.4.8 # via matplotlib matplotlib==3.10.0 # via ase -more-itertools==10.5.0 +more-itertools==10.6.0 # via parsnip (pyproject.toml) numpy==2.2.1 # via @@ -30,7 +30,8 @@ packaging==24.2 # via # matplotlib # pytest -pillow==11.0.0 + # pytest-doctestplus +pillow==11.1.0 # via matplotlib pluggy==1.5.0 # via pytest @@ -41,10 +42,14 @@ pycifrw==4.4.6 pyparsing==3.2.1 # via matplotlib pytest==8.3.4 + # via + # -r tests/requirements.in + # pytest-doctestplus +pytest-doctestplus==1.3.0 # via -r tests/requirements.in python-dateutil==2.9.0.post0 # via matplotlib -scipy==1.14.1 +scipy==1.15.1 # via ase six==1.17.0 # via python-dateutil diff --git a/.github/requirements-3.12.txt b/.github/requirements-3.12.txt index 733d950a..1fa3c123 100644 --- a/.github/requirements-3.12.txt +++ b/.github/requirements-3.12.txt @@ -16,7 +16,7 @@ kiwisolver==1.4.8 # via matplotlib matplotlib==3.10.0 # via ase -more-itertools==10.5.0 +more-itertools==10.6.0 # via parsnip (pyproject.toml) numpy==2.2.1 # via @@ -30,7 +30,8 @@ packaging==24.2 # via # matplotlib # pytest -pillow==11.0.0 + # pytest-doctestplus +pillow==11.1.0 # via matplotlib pluggy==1.5.0 # via pytest @@ -41,10 +42,14 @@ pycifrw==4.4.6 pyparsing==3.2.1 # via matplotlib pytest==8.3.4 + # via + # -r tests/requirements.in + # pytest-doctestplus +pytest-doctestplus==1.3.0 # via -r tests/requirements.in python-dateutil==2.9.0.post0 # via matplotlib -scipy==1.14.1 +scipy==1.15.1 # via ase six==1.17.0 # via python-dateutil diff --git a/.github/requirements-3.13.txt b/.github/requirements-3.13.txt index 4af3370f..aaab9daf 100644 --- a/.github/requirements-3.13.txt +++ b/.github/requirements-3.13.txt @@ -16,7 +16,7 @@ kiwisolver==1.4.8 # via matplotlib matplotlib==3.10.0 # via ase -more-itertools==10.5.0 +more-itertools==10.6.0 # via parsnip (pyproject.toml) numpy==2.2.1 # via @@ -30,7 +30,8 @@ packaging==24.2 # via # matplotlib # pytest -pillow==11.0.0 + # pytest-doctestplus +pillow==11.1.0 # via matplotlib pluggy==1.5.0 # via pytest @@ -41,10 +42,14 @@ pycifrw==4.4.6 pyparsing==3.2.1 # via matplotlib pytest==8.3.4 + # via + # -r tests/requirements.in + # pytest-doctestplus +pytest-doctestplus==1.3.0 # via -r tests/requirements.in python-dateutil==2.9.0.post0 # via matplotlib -scipy==1.14.1 +scipy==1.15.1 # via ase six==1.17.0 # via python-dateutil diff --git a/.github/requirements-3.7.txt b/.github/requirements-3.7.txt index 0328d0fe..8b786c9d 100644 --- a/.github/requirements-3.7.txt +++ b/.github/requirements-3.7.txt @@ -44,9 +44,11 @@ pycifrw==4.4.6 pyparsing==3.1.4 # via matplotlib pytest==7.4.4 + # via matplotlib +pytest-doctestplus==1.0.0 # via -r tests/requirements.in python-dateutil==2.9.0.post0 - # via matplotlib + # via -r tests/requirements.in scipy==1.7.3 # via ase six==1.17.0 diff --git a/.github/requirements-3.8.txt b/.github/requirements-3.8.txt index 8d4e11cf..d753427e 100644 --- a/.github/requirements-3.8.txt +++ b/.github/requirements-3.8.txt @@ -34,6 +34,7 @@ packaging==24.2 # via # matplotlib # pytest + # pytest-doctestplus pillow==10.4.0 # via matplotlib pluggy==1.5.0 @@ -45,6 +46,10 @@ pycifrw==4.4.6 pyparsing==3.1.4 # via matplotlib pytest==8.3.4 + # via + # -r tests/requirements.in + # pytest-doctestplus +pytest-doctestplus==1.3.0 # via -r tests/requirements.in python-dateutil==2.9.0.post0 # via matplotlib diff --git a/.github/requirements-3.9.txt b/.github/requirements-3.9.txt index 892013a4..8ee932f7 100644 --- a/.github/requirements-3.9.txt +++ b/.github/requirements-3.9.txt @@ -12,7 +12,7 @@ fonttools==4.55.3 # via matplotlib gemmi==0.7.0 # via -r tests/requirements.in -importlib-resources==6.4.5 +importlib-resources==6.5.2 # via matplotlib iniconfig==2.0.0 # via pytest @@ -20,7 +20,7 @@ kiwisolver==1.4.7 # via matplotlib matplotlib==3.9.4 # via ase -more-itertools==10.5.0 +more-itertools==10.6.0 # via parsnip (pyproject.toml) numpy==2.0.2 # via @@ -34,7 +34,8 @@ packaging==24.2 # via # matplotlib # pytest -pillow==11.0.0 + # pytest-doctestplus +pillow==11.1.0 # via matplotlib pluggy==1.5.0 # via pytest @@ -45,6 +46,10 @@ pycifrw==4.4.6 pyparsing==3.2.1 # via matplotlib pytest==8.3.4 + # via + # -r tests/requirements.in + # pytest-doctestplus +pytest-doctestplus==1.3.0 # via -r tests/requirements.in python-dateutil==2.9.0.post0 # via matplotlib diff --git a/ChangeLog.rst b/ChangeLog.rst index 7e429929..57ccded9 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -4,7 +4,31 @@ Changelog The format is based on `Keep a Changelog `__. This project adheres to `Semantic Versioning `__. -v0.x.x - 20xx-xx-xx +v1.0.0 - 20xx-xx-xx +------------------- Added ~~~~~ +- Support for nonsimple (';'-delimited) data entries. +- Improved support for entries containing special characters. +- Ability to query multiple keys or columns simultaneously. +- Additional tests for AMCSD and zeolite databases. +- Additional documentation and examples for the new interface + +Changed +~~~~~~~ +- Primary interface is now the ``CifFile`` object, which supports all previously implemented features in addition to several new methods. +- Files are now parsed lazily, and are traversed a single time. + +Dependencies +~~~~~~~~~~~~ +- Added ``more-itertools`` as a dependency for ``peekable`` iterators + + +v0.1.0 - 2024-12-20 +------------------- + +Added +~~~~~ +- Unitcells module +- Function-based parsing interface for key and table reading diff --git a/doc/requirements.in b/doc/requirements.in index d2586601..d64ee66d 100644 --- a/doc/requirements.in +++ b/doc/requirements.in @@ -2,3 +2,6 @@ autodocsumm furo numpy>=1.26.4 sphinx>=7.3.7 +sphinx-copybutton +sphinx-notfound-page +pytest-doctestplus diff --git a/doc/requirements.txt b/doc/requirements.txt index afdf45c7..edd5ca6e 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -10,7 +10,7 @@ beautifulsoup4==4.12.3 # via furo certifi==2024.12.14 # via requests -charset-normalizer==3.4.0 +charset-normalizer==3.4.1 # via requests docutils==0.21.2 # via sphinx @@ -20,20 +20,33 @@ idna==3.10 # via requests imagesize==1.4.1 # via sphinx -jinja2==3.1.4 +iniconfig==2.0.0 + # via pytest +jinja2==3.1.5 # via sphinx markupsafe==3.0.2 # via jinja2 -numpy==2.2.0 +more-itertools==10.6.0 + # via parsnip (pyproject.toml) +numpy==2.2.1 # via # -r doc/requirements.in # parsnip (pyproject.toml) packaging==24.2 - # via sphinx -pygments==2.18.0 + # via + # pytest + # pytest-doctestplus + # sphinx +pluggy==1.5.0 + # via pytest +pygments==2.19.1 # via # furo # sphinx +pytest==8.3.4 + # via pytest-doctestplus +pytest-doctestplus==1.3.0 + # via -r doc/requirements.in requests==2.32.3 # via sphinx snowballstemmer==2.2.0 @@ -46,8 +59,14 @@ sphinx==8.1.3 # autodocsumm # furo # sphinx-basic-ng + # sphinx-copybutton + # sphinx-notfound-page sphinx-basic-ng==1.0.0b2 # via furo +sphinx-copybutton==0.5.2 + # via -r doc/requirements.in +sphinx-notfound-page==1.0.4 + # via -r doc/requirements.in sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -60,5 +79,5 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -urllib3==2.2.3 +urllib3==2.3.0 # via requests diff --git a/doc/source/conf.py b/doc/source/conf.py index 5f271c70..c787f09a 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,5 +1,4 @@ """Configuration file for the Sphinx documentation builder.""" - import datetime CURRENT_YEAR = datetime.date.today().year @@ -22,11 +21,19 @@ extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosummary", + "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", + "sphinx_copybutton", + "pytest_doctestplus.sphinx.doctestplus", "autodocsumm", + "notfound.extension", ] +copybutton_prompt_text = ">>> " +copybutton_remove_prompts = True +copybutton_line_continuation_character = "\\" + templates_path = ["_templates"] exclude_patterns = ["build", "Thumbs.db", ".DS_Store"] intersphinx_mapping = { diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 223042fc..f79f00f3 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -11,48 +11,51 @@ Let's assume we have the file my_file.cif in the current directory, and these ar Reading Keys ^^^^^^^^^^^^ - Now, let's read extract the key-value pairs from our cif file. This subset of data usually contains information to reconstruct the system's unit cell, and provides information regarding the origin of the data. -.. code-block:: python - - from parsnip import CifFile - filename = "my_file.cif" - cif = CifFile(filename) - print(cif.pairs) - ... { - ... '_journal_year': '1999', - ... '_journal_page_first': '0', - ... '_journal_page_last': '123', - ... '_chemical_name_mineral': "'Copper FCC'", - ... '_chemical_formula_sum': "'Cu'", - ... '_cell_length_a': '3.6', - ... '_cell_length_b': '3.6', - ... '_cell_length_c': '3.6', - ... '_cell_angle_alpha': '90.0', - ... '_cell_angle_beta': '90.0', - ... '_cell_angle_gamma': '90.0' - ... '_symmetry_space_group_name_H-M': 'Fm-3m' - ... } + +.. testsetup:: + + >>> import os + >>> if "doc/source" not in os.getcwd(): os.chdir("doc/source") + +.. doctest:: + + >>> from parsnip import CifFile + >>> filename = "example_file.cif" + >>> cif = CifFile(filename) + + >>> cif.pairs + {'_journal_year': '1999', + '_journal_page_first': '0', + '_journal_page_last': '123', + '_chemical_name_mineral': "'Copper FCC'", + '_chemical_formula_sum': "'Cu'", + '_cell_length_a': '3.6', + '_cell_length_b': '3.6', + '_cell_length_c': '3.6', + '_cell_angle_alpha': '90.0', + '_cell_angle_beta': '90.0', + '_cell_angle_gamma': '90.0', + '_symmetry_space_group_name_H-M': "'Fm-3m'"} + A `dict`-like getter syntax is provided to key-value pairs. Single keys function exactly as a python dict, while lists of keys return lists of values. Keys not present in the :attr:`~.pairs` dict instead return :code:`None`. -.. code-block:: python +.. doctest:: - cif["_journal_year"] - ... "1999" + >>> cif["_journal_year"] + '1999' - cif["_not_in_pairs"] - ... None + >>> assert cif["_not_in_pairs"] is None # Multiple keys can be accessed simultaneously! - cif[["_cell_length_a", "_cell_length_b", "_cell_length_c"]] - - ... ["3.6", "3.6", "3.6"] + >>> cif[["_cell_length_a", "_cell_length_b", "_cell_length_c"]] + ['3.6', '3.6', '3.6'] Note that all data is stored and returned as strings by default. It is not generally feasible to determine whether a piece of data should be processed, as conversions may @@ -60,16 +63,15 @@ be lossy. Setting the :attr:`~.cast_values` property to :code:`True` reprocesses data, converting to float or int where possible. Note that once data is reprocessed, a new CifFile object must be created to restore the original string data -.. code-block:: python - - cf.cast_values = True # Reprocesses our `pairs` dict +.. doctest:: - cif["_journal_year"] - ... 1999 + >>> cif.cast_values = True # Reprocess our `pairs` dict - cif[["_cell_length_a", "_cell_length_b", "_cell_length_c"]] + >>> cif["_journal_year"] + 1999 - ... [3.6, 3.6, 3.6] + >>> cif[["_cell_length_a", "_cell_length_b", "_cell_length_c"]] + [3.6, 3.6, 3.6] Reading Tables @@ -93,37 +95,31 @@ list of such arrays, although the :attr:`~.get_from_loops` method is often more convenient. -.. code-block:: python - - - len(cif.loops) - ... 2 +.. doctest:: - cif.loops[0] - ... array( - ... [[('Cu1', '0.0000000000', '0.0000000000', '0.0000000000', 'Cu', 'a')]], - ... dtype=[ - ... ('_atom_site_label', '>> len(cif.loops) + 2 - cif.loops[0]["_atom_site_label"] - ... array([['Cu1']], dtype='>> cif.loops[0] + array([[('Cu1', '0.0000000000', '0.0000000000', '0.0000000000', 'Cu', 'a')]], + dtype=[('_atom_site_label', '>> cif.loops[0]["_atom_site_label"] + array([['Cu1']], dtype='>> xyz = cif.get_from_loops(["_atom_site_fract_x", "_atom_site_fract_y", "_atom_site_fract_z"]) - print(xyz) - ... array([['0.0000000000', '0.0000000000', '0.0000000000']], dtype='>> xyz + array([['0.0000000000', '0.0000000000', '0.0000000000']], dtype='>> xyz.astype(float) + array([[0., 0., 0.]]) @@ -145,14 +141,26 @@ and symmetry-irreducible (Wyckoff) positions contained in the file. Only one line is required to build a tilable unit cell! The positions returned here are in fractional coordinates, and can be imported into tools like `freud`_ to rapidly build out supercells. For absolute coordinates (based on cell parameters stored in the -file), set :code:`fractional=False` +file), set :code:`fractional=False`. .. _`freud`: https://freud.readthedocs.io/en/latest/modules/data.html#freud.data.UnitCell -.. code-block:: python +.. doctest:: + + >>> pos = cif.build_unit_cell(fractional=True) + >>> print(pos) + [[0. 0. 0. ] + [0. 0.5 0.5] + [0.5 0. 0.5] + [0.5 0.5 0. ]] + +Once `freud`_ is installed, crystal structures can be easily replicated! + +.. doctest-requires:: freud - pos = cif.build_unit_cell(fractional=True) - ... array([[0. , 0. , 0. ], - ... [0. , 0.5, 0.5], - ... [0.5, 0. , 0.5], - ... [0.5, 0.5, 0. ]]) + >>> import freud + >>> box = freud.Box(*cif.box) + >>> uc = freud.data.UnitCell(box, basis_positions=pos) + >>> box, pos = uc.generate_system(num_replicas=2) + >>> assert len(pos) == 4 * 2**3 + >>> np.testing.assert_allclose(box.L / 2, 3.6, atol=1e-15) diff --git a/parsnip/conftest.py b/parsnip/conftest.py index d2a7e29a..baff48df 100644 --- a/parsnip/conftest.py +++ b/parsnip/conftest.py @@ -13,5 +13,9 @@ # Set up doctests @pytest.fixture(autouse=True) def _setup_doctest(doctest_namespace): + import os + + if "doc/source" not in os.getcwd(): + os.chdir("doc/source") doctest_namespace["np"] = np - doctest_namespace["cif"] = CifFile("doc/source/example_file.cif") + doctest_namespace["cif"] = CifFile("example_file.cif") diff --git a/parsnip/parsnip.py b/parsnip/parsnip.py index 0349298d..616a1fb1 100644 --- a/parsnip/parsnip.py +++ b/parsnip/parsnip.py @@ -63,6 +63,7 @@ from parsnip._errors import ParseWarning from parsnip.patterns import ( _accumulate_nonsimple_data, + _box_from_lengths_and_angles, _dtype_from_int, _is_data, _is_key, @@ -86,9 +87,9 @@ class CifFile: To get started, simply provide a filename: >>> from parsnip import CifFile - >>> cif = CifFile("doc/source/example_file.cif") + >>> cif = CifFile("example_file.cif") >>> print(cif) - CifFile(fn=doc/source/example_file.cif) : 12 data entries, 2 data loops + CifFile(fn=example_file.cif) : 12 data entries, 2 data loops Data entries are accessible via the :attr:`~.pairs` and :attr:`~.loops` attributes: @@ -302,28 +303,13 @@ def __getitem__(self, index: str | Iterable[str]): if isinstance(index, Iterable) and not isinstance(index, str): return [self.pairs.get(k, None) for k in index] - return self.pairs[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. .. _`unit cell parameters`: https://www.iucr.org/__data/iucr/cifdic_html/1/cif_core.dic/Ccell.html - - Example - ------- - The data returned from this function can be used to create a `freud Box`_. - - .. _`freud Box`: https://freud.readthedocs.io/en/latest/gettingstarted/examples/module_intros/box.Box.html - - >>> cell = cif.read_cell_params(degrees=False) - >>> print(cell) - (3.6, 3.6, 3.6, 1.5707963, 1.5707963, 1.5707963) - >>> assert cell==cif.cell - >>> import freud # doctest: +SKIP - >>> box = freud.box.Box.from_box_lengths_and_angles(cell) # doctest: +SKIP - freud.box.Box(Lx=3.6, Ly=3.6, Lz=3.6, xy=0, xz=0, yz=0, ...) # doctest: +SKIP - Parameters ---------- degrees : bool, optional @@ -337,7 +323,7 @@ def read_cell_params(self, degrees: bool = True, mmcif: bool = False): Returns ------- tuple[float]: - The box vector lengths in angstroms, and angles in degrees or radians + The box vector lengths (in angstroms) and angles (in degrees or radians) :math:`(L_1, L_2, L_3, \alpha, \beta, \gamma)`. Raises @@ -361,7 +347,10 @@ def read_cell_params(self, degrees: bool = True, mmcif: bool = False): "_cell_length_c", *angle_keys, ) - cell_data = cast_array_to_float(arr=self[box_keys], dtype=np.float64) + if self.cast_values: + cell_data = [float(x) for x in self[box_keys]] + else: + cell_data = cast_array_to_float(arr=self[box_keys], dtype=np.float64) def angle_is_invalid(x: float): return x <= 0.0 or x >= 180.0 @@ -537,12 +526,41 @@ def cast_values(self, cast: bool): self._cast_values = cast @property - def cell(self): - """Read the unit cell lengths in Angstroms and angles in radians. + def box(self): + """Read the unit cell as a `freud`_ or HOOMD `box-like`_ object. + + .. _`box-like`: https://hoomd-blue.readthedocs.io/en/v5.0.0/hoomd/module-box.html#hoomd.box.box_like + .. _`freud`: https://freud.readthedocs.io/en/latest/gettingstarted/examples/module_intros/box.Box.html + + .. important:: - Alias for :code:`read_cell_params(degrees=False, mmcif=False)` + ``cif.box`` returns box extents and tilt factors, while + ``CifFile.read_cell_params`` returns unit cell vector lengths and angles. + See the `box-like`_ documentation linked above for more details. + + Example + ------- + This method provides a convinient interface to create box objects. + + >>> box = cif.box + >>> print(box) + (3.6, 3.6, 3.6, 0.0, 0.0, 0.0) + >>> import freud, hoomd # doctest: +SKIP + >>> freud.Box(*box) # doctest: +SKIP + freud.box.Box(Lx=3.6, Ly=3.6, Lz=3.6, xy=0, xz=0, yz=0, ...) + >>> hoomd.Box(*box) # doctest: +SKIP + hoomd.box.Box(Lx=3.6, Ly=3.6, Lz=3.6, xy=0.0, xz=0.0, yz=0.0) + + + Returns + ------- + tuple[float]: + The box vector lengths (in angstroms) and unitless tilt factors. + :math:`(L_1, L_2, L_3, xy, xz, yz)`. """ - return self.read_cell_params(degrees=False, mmcif=False) + return _box_from_lengths_and_angles( + *self.read_cell_params(degrees=False, mmcif=False) + ) @classmethod def structured_to_unstructured(cls, arr: np.ndarray): diff --git a/parsnip/patterns.py b/parsnip/patterns.py index ab25310d..b3cddeca 100644 --- a/parsnip/patterns.py +++ b/parsnip/patterns.py @@ -99,6 +99,9 @@ def cast_array_to_float(arr: ArrayLike, dtype: type = np.float32): ------- np.array[dtype]: Array with new dtype and no significant digit information. """ + arr = [(el if el is not None else "nan") for el in arr] + # if any(el is None for el in arr): + # raise TypeError("Input array contains `None` and cannot be cast!") return np.char.partition(arr, "(")[..., 0].astype(dtype) @@ -177,5 +180,26 @@ def _matrix_from_lengths_and_angles(l1, l2, l3, alpha, beta, gamma): if under_sqrt < 0: raise ValueError("The provided angles can not form a valid box.") a3z = np.sqrt(under_sqrt) + a2 = np.array([l2 * np.cos(gamma), l2 * np.sin(gamma), 0]) a3 = np.array([l3 * a3x, l3 * a3y, l3 * a3z]) + return np.array([a1, a2, a3]) + + +def _box_from_lengths_and_angles(l1, l2, l3, alpha, beta, gamma): + lx = l1 + ly = l2 * np.sin(gamma) + + a3y = (np.cos(alpha) - np.cos(beta) * np.cos(gamma)) / np.sin(gamma) + + lz = l3 * np.sqrt(1 - np.cos(beta) ** 2 - a3y**2) + + a2x = (l3 * l1 * np.cos(beta)) / lx + b = l2 * np.cos(gamma) + c = b * a2x + ly * l3 * a3y + + xy = np.cos(gamma) / np.sin(gamma) + xz = a2x / lz + yz = (c - b * a2x) / (ly * lz) + + return tuple(float(x) for x in [lx, ly, lz, xy, xz, yz]) diff --git a/pyproject.toml b/pyproject.toml index af2c61fd..52d4343e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,9 +36,11 @@ optional-dependencies = {tests = { file = ["tests/requirements.in"] }, doc = { f [tool.pytest.ini_options] testpaths = ["tests", "parsnip", "doc"] +addopts = ["--doctest-plus", "--doctest-glob='*.rst'"] +doctest_rst = true +doctest_optionflags = ["NORMALIZE_WHITESPACE", "ELLIPSIS", "FLOAT_CMP"] console_output_style = "progress" -addopts = ["--doctest-modules", "--doctest-glob='*.rst'"] -doctest_optionflags = ["NORMALIZE_WHITESPACE", "ELLIPSIS", "NUMBER"] + [tool.ruff] include = ["*.py", "*.ipynb"] diff --git a/test/output.txt b/test/output.txt new file mode 100644 index 00000000..39dfbb0f --- /dev/null +++ b/test/output.txt @@ -0,0 +1,26 @@ +Results of doctest builder run on 2025-01-14 12:57:15 +===================================================== + +Document: package-parse +----------------------- +********************************************************************** +File "../../parsnip/parsnip.py", line ?, in default +Failed example: + print(cell) +Expected: + (3.6, 3.6, 3.6, 1.5707963, 1.5707963, 1.5707963) +Got: + (3.6, 3.6, 3.6, 1.5707963267948966, 1.5707963267948966, 1.5707963267948966) +********************************************************************** +1 items had failures: + 1 of 18 in default +18 tests in 1 items. +17 passed and 1 failed. +***Test Failed*** 1 failures. + +Doctest summary +=============== + 18 tests + 1 failure in tests + 0 failures in setup code + 0 failures in cleanup code diff --git a/tests/requirements.in b/tests/requirements.in index bbe5f4d4..1902320f 100644 --- a/tests/requirements.in +++ b/tests/requirements.in @@ -2,3 +2,4 @@ ase gemmi pycifrw pytest +pytest-doctestplus diff --git a/tests/test_patterns.py b/tests/test_patterns.py index 088fc5b6..f941e707 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -1,8 +1,10 @@ import numpy as np import pytest +from conftest import cif_files_mark from parsnip._errors import ParseWarning from parsnip.patterns import ( + _box_from_lengths_and_angles, _dtype_from_int, _is_data, _is_key, @@ -198,3 +200,27 @@ def test_try_cast_to_numeric(s): assert isinstance(result, float) else: assert isinstance(result, int) + + +@cif_files_mark +def test_box(cif_data): + freud = pytest.importorskip("freud") + + cif_box = cif_data.file.read_cell_params( + degrees=False, mmcif="PDB" in cif_data.filename + ) + + freud_box = freud.Box.from_box_lengths_and_angles(*cif_box) + freud_box_2 = freud.Box(*cif_data.file.box) + parsnip_box = _box_from_lengths_and_angles(*cif_box) + + np.testing.assert_allclose(parsnip_box[:3], freud_box.L, atol=1e-15) + np.testing.assert_allclose( + parsnip_box[3:], [freud_box.xy, freud_box.xz, freud_box.yz], atol=1e-15 + ) + if "PDB" not in cif_data.filename: + np.testing.assert_allclose( + [*freud_box.L, freud_box.xy, freud_box.xz, freud_box.yz], + [*freud_box_2.L, freud_box_2.xy, freud_box_2.xz, freud_box_2.yz], + atol=1e-8, + ) diff --git a/tests/test_unitcells.py b/tests/test_unitcells.py index 77596df2..27f103a4 100644 --- a/tests/test_unitcells.py +++ b/tests/test_unitcells.py @@ -39,11 +39,6 @@ def test_read_cell_params(cif_data, keys=box_keys): gemmi_data = _gemmi_read_keys(cif_data.filename, keys) np.testing.assert_array_equal(parsnip_data, gemmi_data) - if not mmcif: - np.testing.assert_array_equal( - cif_data.file.cell[3:], np.deg2rad(gemmi_data[3:]) - ) - @cif_files_mark def test_read_symmetry_operations(cif_data):