From 6dfb4a7226def464a397b682b8716170453d4845 Mon Sep 17 00:00:00 2001 From: clpetix Date: Mon, 2 Jun 2025 14:29:00 -0500 Subject: [PATCH 1/5] Add from_matrix class method to Box. --- src/lammpsio/box.py | 49 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_box.py | 30 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/lammpsio/box.py b/src/lammpsio/box.py index 1e4e09a1..1a55657e 100644 --- a/src/lammpsio/box.py +++ b/src/lammpsio/box.py @@ -63,6 +63,55 @@ def cast(cls, value): else: raise TypeError(f"Unable to cast boxlike object with shape {v.shape}") + @classmethod + def from_matrix(cls, low, matrix): + """Create a Box from low and matrix. + + Parameters + ---------- + low : list + Origin of the box. + matrix : :class:`numpy.ndarray` + Upper triangular matrix in LAMMPS style: + [[lx, xy, xz], + [0, ly, yz], + [0, 0, lz]] + + Returns + ------- + :class:`Box` + A simulation box. + + Raises + ------ + TypeError + If `low` is not length 3. + TypeError + If `matrix` is not a 3x3 array. + ValueError + If `matrix` is not upper triangular. + + """ + low = numpy.array(low, dtype=float) + arr = numpy.array(matrix, dtype=float) + + if low.shape != (3,): + raise TypeError("Low must be a 3-tuple") + if arr.shape != (3, 3): + raise TypeError("Box matrix must be a 3x3 array") + if arr[1, 0] != 0 or arr[2, 0] != 0 or arr[2, 1] != 0: + raise ValueError("Box matrix must be upper triangular") + + # Extract diagonal elements for box lengths + lx, ly, lz = arr[0, 0], arr[1, 1], arr[2, 2] + high = low + [lx, ly, lz] + + # Extract tilt factors + xy, xz, yz = arr[0, 1], arr[0, 2], arr[1, 2] + tilt = [xy, xz, yz] if numpy.any([xy, xz, yz]) else None + + return cls(low, high, tilt) + @property def low(self): """:class:`numpy.ndarray`: Box low.""" diff --git a/tests/test_box.py b/tests/test_box.py index aaa612c5..f8570abc 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -1,5 +1,8 @@ import numpy import pytest +from pytest_lazy_fixtures import lf + +import lammpsio def test_orthorhombic(orthorhombic): @@ -39,3 +42,30 @@ def test_triclinic(triclinic): assert numpy.allclose(box.tilt, [0, 0, 0]) with pytest.raises(TypeError): box.tilt = [0, 0] + + +@pytest.mark.parametrize("box", [lf("orthorhombic"), lf("triclinic")]) +def test_from_matrix(box): + lx, ly, lz = box.high - box.low + xy, xz, yz = box.tilt if box.tilt is not None else (0, 0, 0) + matrix = numpy.array([[lx, xy, xz], [0, ly, yz], [0, 0, lz]]) + new_box = lammpsio.Box.from_matrix(box.low, matrix) + + assert numpy.allclose(new_box.low, box.low) + assert numpy.allclose(new_box.high, box.high) + if box.tilt is not None: + assert numpy.allclose(new_box.tilt, box.tilt) + + # test with invalid low + with pytest.raises(TypeError): + lammpsio.Box.from_matrix([0, 0], matrix) + + # test with invalid matrix shape + invalid_matrix_shape = numpy.array([[lx, xy], [ly, yz]]) + with pytest.raises(TypeError): + lammpsio.Box.from_matrix(box.low, invalid_matrix_shape) + + # test with invalid matrix values + invalid_matrix = numpy.array([[lx, xy, xz], [ly, 0, yz], [lz, 0, 0]]) + with pytest.raises(ValueError): + lammpsio.Box.from_matrix(box.low, invalid_matrix) From 553a041894ebd37adc74f434af9bc00e0ed88f0f Mon Sep 17 00:00:00 2001 From: clpetix Date: Tue, 3 Jun 2025 10:29:44 -0500 Subject: [PATCH 2/5] Fix failing documentation. --- src/lammpsio/box.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lammpsio/box.py b/src/lammpsio/box.py index 1a55657e..6cd4b04e 100644 --- a/src/lammpsio/box.py +++ b/src/lammpsio/box.py @@ -72,10 +72,11 @@ def from_matrix(cls, low, matrix): low : list Origin of the box. matrix : :class:`numpy.ndarray` - Upper triangular matrix in LAMMPS style: - [[lx, xy, xz], - [0, ly, yz], - [0, 0, lz]] + Upper triangular matrix in LAMMPS style:: + + [[lx, xy, xz], + [0, ly, yz], + [0, 0, lz]] Returns ------- From f41e5ceda4917d65d4aa5081e40e4157aa6c9ac8 Mon Sep 17 00:00:00 2001 From: clpetix Date: Tue, 3 Jun 2025 11:14:51 -0500 Subject: [PATCH 3/5] Add force_triclinic to allow zero tilt factors, if desired. --- src/lammpsio/box.py | 15 ++++++++++----- tests/test_box.py | 18 ++++++++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/lammpsio/box.py b/src/lammpsio/box.py index 6cd4b04e..17121513 100644 --- a/src/lammpsio/box.py +++ b/src/lammpsio/box.py @@ -64,7 +64,7 @@ def cast(cls, value): raise TypeError(f"Unable to cast boxlike object with shape {v.shape}") @classmethod - def from_matrix(cls, low, matrix): + def from_matrix(cls, low, matrix, force_triclinic=False): """Create a Box from low and matrix. Parameters @@ -77,6 +77,9 @@ def from_matrix(cls, low, matrix): [[lx, xy, xz], [0, ly, yz], [0, 0, lz]] + force_triclinic : bool, optional + If ``True``, forces the box to be triclinic even if the tilt + factors are zero. Default is ``False``. Returns ------- @@ -103,13 +106,15 @@ def from_matrix(cls, low, matrix): if arr[1, 0] != 0 or arr[2, 0] != 0 or arr[2, 1] != 0: raise ValueError("Box matrix must be upper triangular") - # Extract diagonal elements for box lengths - lx, ly, lz = arr[0, 0], arr[1, 1], arr[2, 2] - high = low + [lx, ly, lz] + # Calculate high from the matrix + high = low + numpy.diag(arr) # Extract tilt factors xy, xz, yz = arr[0, 1], arr[0, 2], arr[1, 2] - tilt = [xy, xz, yz] if numpy.any([xy, xz, yz]) else None + if force_triclinic: + tilt = [xy, xz, yz] if numpy.any([xy, xz, yz]) else [0, 0, 0] + else: + tilt = [xy, xz, yz] if numpy.any([xy, xz, yz]) else None return cls(low, high, tilt) diff --git a/tests/test_box.py b/tests/test_box.py index f8570abc..5688a2e4 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -45,16 +45,26 @@ def test_triclinic(triclinic): @pytest.mark.parametrize("box", [lf("orthorhombic"), lf("triclinic")]) -def test_from_matrix(box): +@pytest.mark.parametrize("force_triclinic", [True, False]) +def test_from_matrix(box, force_triclinic): lx, ly, lz = box.high - box.low xy, xz, yz = box.tilt if box.tilt is not None else (0, 0, 0) matrix = numpy.array([[lx, xy, xz], [0, ly, yz], [0, 0, lz]]) - new_box = lammpsio.Box.from_matrix(box.low, matrix) + new_box = lammpsio.Box.from_matrix(box.low, matrix, force_triclinic=force_triclinic) assert numpy.allclose(new_box.low, box.low) assert numpy.allclose(new_box.high, box.high) - if box.tilt is not None: - assert numpy.allclose(new_box.tilt, box.tilt) + if force_triclinic: + assert new_box.tilt is not None + if box.tilt is not None: + assert numpy.allclose(new_box.tilt, box.tilt) + else: + assert numpy.allclose(new_box.tilt, [0, 0, 0]) + else: + if box.tilt is not None: + assert numpy.allclose(new_box.tilt, box.tilt) + else: + assert new_box.tilt is None # test with invalid low with pytest.raises(TypeError): From b53057043f1581f23e00f4ca64d4ef732eb2bda0 Mon Sep 17 00:00:00 2001 From: clpetix Date: Tue, 3 Jun 2025 11:25:53 -0500 Subject: [PATCH 4/5] force_triclinic documentation tweek. --- src/lammpsio/box.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lammpsio/box.py b/src/lammpsio/box.py index 17121513..bce982c4 100644 --- a/src/lammpsio/box.py +++ b/src/lammpsio/box.py @@ -77,7 +77,7 @@ def from_matrix(cls, low, matrix, force_triclinic=False): [[lx, xy, xz], [0, ly, yz], [0, 0, lz]] - force_triclinic : bool, optional + force_triclinic : bool If ``True``, forces the box to be triclinic even if the tilt factors are zero. Default is ``False``. From c9ef3488bdce81e11e252a9fc5f4e9432d294779 Mon Sep 17 00:00:00 2001 From: clpetix Date: Tue, 3 Jun 2025 15:49:20 -0500 Subject: [PATCH 5/5] Reduce code duplication in from_matrix. --- src/lammpsio/box.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lammpsio/box.py b/src/lammpsio/box.py index bce982c4..3965c743 100644 --- a/src/lammpsio/box.py +++ b/src/lammpsio/box.py @@ -110,11 +110,9 @@ def from_matrix(cls, low, matrix, force_triclinic=False): high = low + numpy.diag(arr) # Extract tilt factors - xy, xz, yz = arr[0, 1], arr[0, 2], arr[1, 2] - if force_triclinic: - tilt = [xy, xz, yz] if numpy.any([xy, xz, yz]) else [0, 0, 0] - else: - tilt = [xy, xz, yz] if numpy.any([xy, xz, yz]) else None + tilt = [arr[0, 1], arr[0, 2], arr[1, 2]] + if not force_triclinic and not numpy.any(tilt): + tilt = None return cls(low, high, tilt)