From 334135c423d08ba5fb3efd4ae7c2ddd20054ef96 Mon Sep 17 00:00:00 2001 From: clpetix Date: Thu, 29 May 2025 13:02:37 -0500 Subject: [PATCH 1/9] Fix box centering in GSD conversion , add unit test, and ensure copy is made of box data instead of reference. --- src/lammpsio/snapshot.py | 14 +++++++------- tests/test_snapshot.py | 10 +++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/lammpsio/snapshot.py b/src/lammpsio/snapshot.py index adfe7732..9a50e0ee 100644 --- a/src/lammpsio/snapshot.py +++ b/src/lammpsio/snapshot.py @@ -68,8 +68,8 @@ def from_hoomd_gsd(cls, frame): frame.validate() # process HOOMD box to LAMMPS box - L = frame.configuration.box[:3] - tilt = frame.configuration.box[3:] + L = frame.configuration.box.copy()[:3] + tilt = frame.configuration.box.copy()[3:] if frame.configuration.dimensions == 3: tilt[0] *= L[1] tilt[1:] *= L[2] @@ -225,12 +225,12 @@ def to_hoomd_gsd(self, type_map=None): if self.step is not None: frame.configuration.step = int(self.step) - # we could shift the box later, but for now this is an error - if not numpy.allclose(-self.box.low, self.box.high): - raise ValueError("GSD boxes must be centered around 0") L = self.box.high - self.box.low + center = 0.5 * (self.box.high + self.box.low) if self.box.tilt is not None: - tilt = self.box.tilt + tilt = self.box.tilt.copy() + tilt[0] /= L[1] + tilt[1:] /= L[2] else: tilt = [0, 0, 0] frame.configuration.box = numpy.concatenate((L, tilt)) @@ -247,7 +247,7 @@ def to_hoomd_gsd(self, type_map=None): frame.particles.N = self.N if self.has_position(): - frame.particles.position = self.position.copy() + frame.particles.position = self.position.copy() - center if self.has_velocity(): frame.particles.velocity = self.velocity.copy() if self.has_image(): diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 8c00252b..719fe9e1 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -99,10 +99,14 @@ def test_gsd_conversion(): snap2.to_hoomd_gsd() assert numpy.all(snap2.id == [2, 1]) - # check for error out on bad box + # check that box is set correctly when not originally centered + snap2.id = [1, 2] + snap2.position = [[0, 0, 0], [1, 1, 1]] snap2.box.low = [-10, -10, -10] - with pytest.raises(ValueError): - snap2.to_hoomd_gsd() + center = (snap2.box.high + snap2.box.low) / 2 + frame5 = snap2.to_hoomd_gsd() + assert numpy.allclose(frame5.configuration.box, [15, 15, 15, 0, 0, 0]) + assert numpy.allclose(frame5.particles.position, snap2.position - center) @pytest.mark.skipif(not has_gsd, reason="gsd not installed") From d16df5db5f4290bc6d275f717f553d55d56e6c5c Mon Sep 17 00:00:00 2001 From: Levi Petix <81758680+clpetix@users.noreply.github.com> Date: Fri, 30 May 2025 10:06:45 -0500 Subject: [PATCH 2/9] Update src/lammpsio/snapshot.py Co-authored-by: Michael Howard --- src/lammpsio/snapshot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lammpsio/snapshot.py b/src/lammpsio/snapshot.py index 9a50e0ee..49cc0726 100644 --- a/src/lammpsio/snapshot.py +++ b/src/lammpsio/snapshot.py @@ -68,8 +68,9 @@ def from_hoomd_gsd(cls, frame): frame.validate() # process HOOMD box to LAMMPS box - L = frame.configuration.box.copy()[:3] - tilt = frame.configuration.box.copy()[3:] + box = numpy.array(frame.configuration.box, copy=True) + L = box[:3] + tilt = box[3:] if frame.configuration.dimensions == 3: tilt[0] *= L[1] tilt[1:] *= L[2] From d7330f38c669f1405022579a83c41bc2ba8b0b97 Mon Sep 17 00:00:00 2001 From: clpetix Date: Fri, 30 May 2025 12:48:03 -0500 Subject: [PATCH 3/9] Fix centering for triclinic box. Fix bug in from_hoomd_gsd. --- src/lammpsio/snapshot.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/lammpsio/snapshot.py b/src/lammpsio/snapshot.py index 49cc0726..0c1031ea 100644 --- a/src/lammpsio/snapshot.py +++ b/src/lammpsio/snapshot.py @@ -79,7 +79,14 @@ def from_hoomd_gsd(cls, frame): # HOOMD boxes can have Lz = 0, but LAMMPS does not allow this. if L[2] == 0: L[2] = 1.0 - box = Box(low=-0.5 * L, high=0.5 * L, tilt=tilt) + + x_edge = numpy.array([L[0], 0, 0]) + y_edge = numpy.array([tilt[0], L[1], 0]) + z_edge = numpy.array([tilt[1], tilt[2], L[2]]) + + low = -0.5 * (x_edge + y_edge + z_edge) + high = low + L + box = Box(low=low, high=high, tilt=tilt) snap = Snapshot( N=frame.particles.N, @@ -227,7 +234,22 @@ def to_hoomd_gsd(self, type_map=None): frame.configuration.step = int(self.step) L = self.box.high - self.box.low - center = 0.5 * (self.box.high + self.box.low) + # Calculate edge vectors using LAMMPS convention + x_edge = numpy.array([L[0], 0, 0]) + y_edge = numpy.array( + [self.box.tilt[0] if self.box.tilt is not None else 0, L[1], 0] + ) + z_edge = numpy.array( + [ + self.box.tilt[1] if self.box.tilt is not None else 0, + self.box.tilt[2] if self.box.tilt is not None else 0, + L[2], + ] + ) + + # Calculate center of box by calculating opposite corner of low + opposite_corner = self.box.low + x_edge + y_edge + z_edge + center = (opposite_corner + self.box.low) / 2.0 if self.box.tilt is not None: tilt = self.box.tilt.copy() tilt[0] /= L[1] From 3bc92627ea22e8a1cfc723250a75bec99e07d579 Mon Sep 17 00:00:00 2001 From: clpetix Date: Fri, 30 May 2025 12:57:28 -0500 Subject: [PATCH 4/9] Add triclinic unit test for autocentering. --- tests/test_snapshot.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 719fe9e1..c73c8912 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -46,8 +46,8 @@ def test_gsd_conversion(): # make Snapshot from GSD snap, type_map = lammpsio.Snapshot.from_hoomd_gsd(frame) assert snap.step == 3 - assert numpy.allclose(snap.box.low, [-2, -2.5, -3]) - assert numpy.allclose(snap.box.high, [2, 2.5, 3]) + assert numpy.allclose(snap.box.low, [-2.85, -3.4, -3]) + assert numpy.allclose(snap.box.high, [1.15, 1.6, 3]) assert numpy.allclose(snap.box.tilt, [0.5, 1.2, 1.8]) assert snap.N == 2 assert numpy.allclose(snap.position, [[0.1, 0.2, 0.3], [-0.1, -0.2, -0.3]]) @@ -99,15 +99,28 @@ def test_gsd_conversion(): snap2.to_hoomd_gsd() assert numpy.all(snap2.id == [2, 1]) - # check that box is set correctly when not originally centered + # check that orthorhombic box is set correctly when not originally centered snap2.id = [1, 2] snap2.position = [[0, 0, 0], [1, 1, 1]] snap2.box.low = [-10, -10, -10] - center = (snap2.box.high + snap2.box.low) / 2 + # center calculated using box vectors as describe in LAMMPS documentation + center = [-2.5, -2.5, -2.5] frame5 = snap2.to_hoomd_gsd() assert numpy.allclose(frame5.configuration.box, [15, 15, 15, 0, 0, 0]) assert numpy.allclose(frame5.particles.position, snap2.position - center) + # check that triclinic box is set correctly when not originally centered + snap3 = lammpsio.Snapshot( + N=2, box=lammpsio.Box([0, 0, 0], [15, 15, 15], [1.5, 3.0, 4.5]) + ) + snap3.id = [1, 2] + snap3.position = [[0, 0, 0], [1, 1, 1]] + # center calculated using box vectors as describe in LAMMPS documentation + center = [9.75, 9.75, 7.5] + frame6 = snap3.to_hoomd_gsd() + assert numpy.allclose(frame6.configuration.box, [15, 15, 15, 0.1, 0.2, 0.3]) + assert numpy.allclose(frame6.particles.position, snap3.position - center) + @pytest.mark.skipif(not has_gsd, reason="gsd not installed") def test_minimal_gsd_conversion(): From ad54fe4040fe7464bc3dc9cd6f108e6a934e5579 Mon Sep 17 00:00:00 2001 From: clpetix Date: Fri, 30 May 2025 13:08:14 -0500 Subject: [PATCH 5/9] White space fix and refactor variables. --- src/lammpsio/snapshot.py | 15 ++++++--------- tests/test_snapshot.py | 17 ++++++----------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/lammpsio/snapshot.py b/src/lammpsio/snapshot.py index 0c1031ea..086a452f 100644 --- a/src/lammpsio/snapshot.py +++ b/src/lammpsio/snapshot.py @@ -234,22 +234,19 @@ def to_hoomd_gsd(self, type_map=None): frame.configuration.step = int(self.step) L = self.box.high - self.box.low - # Calculate edge vectors using LAMMPS convention - x_edge = numpy.array([L[0], 0, 0]) - y_edge = numpy.array( - [self.box.tilt[0] if self.box.tilt is not None else 0, L[1], 0] - ) - z_edge = numpy.array( + # Calculate box vectors a b & c using LAMMPS convention + a = numpy.array([L[0], 0, 0]) + b = numpy.array([self.box.tilt[0] if self.box.tilt is not None else 0, L[1], 0]) + c = numpy.array( [ self.box.tilt[1] if self.box.tilt is not None else 0, self.box.tilt[2] if self.box.tilt is not None else 0, L[2], ] ) - # Calculate center of box by calculating opposite corner of low - opposite_corner = self.box.low + x_edge + y_edge + z_edge - center = (opposite_corner + self.box.low) / 2.0 + far_vertex = self.box.low + a + b + c + center = (far_vertex + self.box.low) / 2.0 if self.box.tilt is not None: tilt = self.box.tilt.copy() tilt[0] /= L[1] diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index c73c8912..1cca1df4 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -100,21 +100,16 @@ def test_gsd_conversion(): assert numpy.all(snap2.id == [2, 1]) # check that orthorhombic box is set correctly when not originally centered - snap2.id = [1, 2] - snap2.position = [[0, 0, 0], [1, 1, 1]] - snap2.box.low = [-10, -10, -10] + snap3 = lammpsio.Snapshot(N=2, box=lammpsio.Box([0, 0, 0], [15, 15, 15])) + snap3.position = [[0, 0, 0], [1, 1, 1]] # center calculated using box vectors as describe in LAMMPS documentation - center = [-2.5, -2.5, -2.5] - frame5 = snap2.to_hoomd_gsd() + center = [7.5, 7.5, 7.5] + frame5 = snap3.to_hoomd_gsd() assert numpy.allclose(frame5.configuration.box, [15, 15, 15, 0, 0, 0]) - assert numpy.allclose(frame5.particles.position, snap2.position - center) + assert numpy.allclose(frame5.particles.position, snap3.position - center) # check that triclinic box is set correctly when not originally centered - snap3 = lammpsio.Snapshot( - N=2, box=lammpsio.Box([0, 0, 0], [15, 15, 15], [1.5, 3.0, 4.5]) - ) - snap3.id = [1, 2] - snap3.position = [[0, 0, 0], [1, 1, 1]] + snap3.box.tilt = [1.5, 3.0, 4.5] # center calculated using box vectors as describe in LAMMPS documentation center = [9.75, 9.75, 7.5] frame6 = snap3.to_hoomd_gsd() From e4ea2c6848a7c283575882f3001493fb470055d9 Mon Sep 17 00:00:00 2001 From: clpetix Date: Tue, 3 Jun 2025 16:56:11 -0500 Subject: [PATCH 6/9] Remove unnecessary copy when converting HOOMD box to numpy array in Snapshot class. --- src/lammpsio/snapshot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lammpsio/snapshot.py b/src/lammpsio/snapshot.py index 086a452f..9d90de61 100644 --- a/src/lammpsio/snapshot.py +++ b/src/lammpsio/snapshot.py @@ -68,7 +68,7 @@ def from_hoomd_gsd(cls, frame): frame.validate() # process HOOMD box to LAMMPS box - box = numpy.array(frame.configuration.box, copy=True) + box = numpy.array(frame.configuration.box) L = box[:3] tilt = box[3:] if frame.configuration.dimensions == 3: From 907a335cd15cddeff06a9c4b363199d9898fe77d Mon Sep 17 00:00:00 2001 From: clpetix Date: Tue, 3 Jun 2025 17:09:41 -0500 Subject: [PATCH 7/9] Simplify centering using box_matrix and put centering of particles under has_particles. --- src/lammpsio/snapshot.py | 41 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/lammpsio/snapshot.py b/src/lammpsio/snapshot.py index 9d90de61..deec3226 100644 --- a/src/lammpsio/snapshot.py +++ b/src/lammpsio/snapshot.py @@ -80,13 +80,11 @@ def from_hoomd_gsd(cls, frame): if L[2] == 0: L[2] = 1.0 - x_edge = numpy.array([L[0], 0, 0]) - y_edge = numpy.array([tilt[0], L[1], 0]) - z_edge = numpy.array([tilt[1], tilt[2], L[2]]) - - low = -0.5 * (x_edge + y_edge + z_edge) - high = low + L - box = Box(low=low, high=high, tilt=tilt) + matrix = numpy.array( + [[L[0], tilt[0], tilt[1]], [0, L[1], tilt[2]], [0, 0, L[2]]] + ) + low = -0.5 * (matrix[:, 0] + matrix[:, 1] + matrix[:, 2]) + box = Box.from_matrix(low=low, matrix=matrix) snap = Snapshot( N=frame.particles.N, @@ -234,19 +232,6 @@ def to_hoomd_gsd(self, type_map=None): frame.configuration.step = int(self.step) L = self.box.high - self.box.low - # Calculate box vectors a b & c using LAMMPS convention - a = numpy.array([L[0], 0, 0]) - b = numpy.array([self.box.tilt[0] if self.box.tilt is not None else 0, L[1], 0]) - c = numpy.array( - [ - self.box.tilt[1] if self.box.tilt is not None else 0, - self.box.tilt[2] if self.box.tilt is not None else 0, - L[2], - ] - ) - # Calculate center of box by calculating opposite corner of low - far_vertex = self.box.low + a + b + c - center = (far_vertex + self.box.low) / 2.0 if self.box.tilt is not None: tilt = self.box.tilt.copy() tilt[0] /= L[1] @@ -267,6 +252,22 @@ def to_hoomd_gsd(self, type_map=None): frame.particles.N = self.N if self.has_position(): + # Calculate box vectors a b & c using LAMMPS convention + a = numpy.array([L[0], 0, 0]) + b = numpy.array( + [self.box.tilt[0] if self.box.tilt is not None else 0, L[1], 0] + ) + c = numpy.array( + [ + self.box.tilt[1] if self.box.tilt is not None else 0, + self.box.tilt[2] if self.box.tilt is not None else 0, + L[2], + ] + ) + # Calculate center of box by calculating opposite corner of low + far_vertex = self.box.low + a + b + c + center = (far_vertex + self.box.low) / 2.0 + # center the positions around the box center frame.particles.position = self.position.copy() - center if self.has_velocity(): frame.particles.velocity = self.velocity.copy() From 115c6cb523148f1309737fb938daa719afd16c91 Mon Sep 17 00:00:00 2001 From: clpetix Date: Wed, 4 Jun 2025 09:17:55 -0500 Subject: [PATCH 8/9] Revert explicit copy in from_hoomd_gsd. --- src/lammpsio/snapshot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lammpsio/snapshot.py b/src/lammpsio/snapshot.py index deec3226..43b9e26c 100644 --- a/src/lammpsio/snapshot.py +++ b/src/lammpsio/snapshot.py @@ -68,7 +68,7 @@ def from_hoomd_gsd(cls, frame): frame.validate() # process HOOMD box to LAMMPS box - box = numpy.array(frame.configuration.box) + box = numpy.array(frame.configuration.box, copy=True) L = box[:3] tilt = box[3:] if frame.configuration.dimensions == 3: From b7b78dfa38fdf0fc398b3b98f3322e747adce7a8 Mon Sep 17 00:00:00 2001 From: clpetix Date: Wed, 4 Jun 2025 11:22:03 -0500 Subject: [PATCH 9/9] Simplify box_matrix code in Snapshot.py. --- src/lammpsio/snapshot.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/lammpsio/snapshot.py b/src/lammpsio/snapshot.py index 43b9e26c..5d13738a 100644 --- a/src/lammpsio/snapshot.py +++ b/src/lammpsio/snapshot.py @@ -83,7 +83,7 @@ def from_hoomd_gsd(cls, frame): matrix = numpy.array( [[L[0], tilt[0], tilt[1]], [0, L[1], tilt[2]], [0, 0, L[2]]] ) - low = -0.5 * (matrix[:, 0] + matrix[:, 1] + matrix[:, 2]) + low = -0.5 * numpy.sum(matrix, axis=1) box = Box.from_matrix(low=low, matrix=matrix) snap = Snapshot( @@ -252,23 +252,16 @@ def to_hoomd_gsd(self, type_map=None): frame.particles.N = self.N if self.has_position(): - # Calculate box vectors a b & c using LAMMPS convention - a = numpy.array([L[0], 0, 0]) - b = numpy.array( - [self.box.tilt[0] if self.box.tilt is not None else 0, L[1], 0] - ) - c = numpy.array( + # Center the positions using HOOMD tilt factors (computed above) + matrix = numpy.array( [ - self.box.tilt[1] if self.box.tilt is not None else 0, - self.box.tilt[2] if self.box.tilt is not None else 0, - L[2], + [L[0], tilt[0] * L[1], tilt[1] * L[2]], + [0, L[1], tilt[2] * L[2]], + [0, 0, L[2]], ] ) - # Calculate center of box by calculating opposite corner of low - far_vertex = self.box.low + a + b + c - center = (far_vertex + self.box.low) / 2.0 - # center the positions around the box center - frame.particles.position = self.position.copy() - center + center = self.box.low + 0.5 * numpy.sum(matrix, axis=1) + frame.particles.position = self.position - center if self.has_velocity(): frame.particles.velocity = self.velocity.copy() if self.has_image():