Skip to content

Commit 210df88

Browse files
Merge pull request #672 from daico007/box_length_bug
Rework box class including NumPy subclass
2 parents 04b9f09 + 3059116 commit 210df88

File tree

3 files changed

+121
-26
lines changed

3 files changed

+121
-26
lines changed

mbuild/box.py

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import numpy as np
44

5+
__all__ = ['Box']
56

67
class Box(object):
78
"""A box representing the bounds of the system.
@@ -24,23 +25,34 @@ def __init__(self, lengths=None, mins=None, maxs=None, angles=None):
2425
"You provided: "
2526
"lengths={} mins={} maxs={}".format(lengths, mins, maxs)
2627
)
27-
self._mins = np.array(mins, dtype=np.float)
28-
self._maxs = np.array(maxs, dtype=np.float)
29-
self._lengths = self.maxs - self.mins
28+
mins = np.array(mins, dtype=np.float)
29+
maxs = np.array(maxs, dtype=np.float)
30+
assert mins.shape == (3, ), "Given mins have wrong dimensions"
31+
assert maxs.shape == (3, ), "Given maxs have wrong dimensions"
32+
assert all(mins <= maxs), "Given mins are greater than maxs"
33+
self._mins = _BoxArray(array=mins, var="mins", box=self)
34+
self._maxs = _BoxArray(array=maxs, var="maxs", box=self)
35+
self._lengths = _BoxArray(array=(self.maxs - self.mins), var="lengths", box=self)
3036
else:
3137
if mins is not None or maxs is not None:
3238
warn(
3339
"Provided `lengths` and `mins` and/or `maxs`. Only `lengths` "
3440
"is being used. You provided: "
3541
"lengths={} mins={} maxs={}".format(lengths, mins, maxs)
3642
)
37-
self._mins = np.array([0.0, 0.0, 0.0])
38-
self._maxs = np.array(lengths, dtype=np.float)
39-
self._lengths = np.array(lengths, dtype=np.float)
43+
if isinstance(lengths, int) or isinstance(lengths, float):
44+
lengths = np.array(lengths*np.ones(3), dtype=np.float)
45+
else:
46+
lengths = np.array(lengths, dtype=np.float)
47+
assert lengths.shape == (3, )
48+
assert all(lengths >= 0), "Given lengths are negative"
49+
self._mins = _BoxArray(array=(0,0,0), var="mins", box=self)
50+
self._maxs = _BoxArray(array=lengths, var="maxs", box=self)
51+
self._lengths = _BoxArray(array=lengths, var="lengths", box=self)
4052
if angles is None:
41-
angles = np.array([90.0, 90.0, 90.0])
53+
angles = _BoxArray(array=(90.0, 90.0, 90.0), var="angles", box=self)
4254
elif isinstance(angles, (list, np.ndarray)):
43-
angles = np.array(angles, dtype=np.float)
55+
angles = _BoxArray(array=angles, var="angles", box=self)
4456
self._angles = angles
4557

4658
@property
@@ -61,35 +73,78 @@ def angles(self):
6173

6274
@mins.setter
6375
def mins(self, mins):
64-
if isinstance(mins, list):
65-
mins = np.array(mins, dtype=np.float)
76+
mins = np.array(mins, dtype=np.float)
6677
assert mins.shape == (3, )
67-
self._mins = mins
68-
self._lengths = self.maxs - self.mins
78+
assert all(mins <= self.maxs), "Given mins is greater than maxs"
79+
self._mins = _BoxArray(array=mins, var="mins", box=self)
80+
self._lengths = _BoxArray(array=(self.maxs - self.mins), var="lengths", box=self)
6981

7082
@maxs.setter
71-
def maxs(self, maxes):
72-
if isinstance(maxes, list):
73-
maxes = np.array(maxes, dtype=np.float)
74-
assert maxes.shape == (3, )
75-
self._maxs = maxes
76-
self._lengths = self.maxs - self.mins
83+
def maxs(self, maxs):
84+
maxs = np.array(maxs, dtype=np.float)
85+
assert maxs.shape == (3, )
86+
assert all(maxs >= self.mins), "Given maxs is less than mins"
87+
self._maxs = _BoxArray(array=maxs, var="maxs", box=self)
88+
self._lengths = _BoxArray(array= (self.maxs - self.mins), var="lengths", box=self)
7789

7890
@lengths.setter
7991
def lengths(self, lengths):
80-
if isinstance(lengths, list):
92+
if isinstance(lengths, int) or isinstance(lengths, float):
93+
lengths = np.array(lengths*np.ones(3), dtype=np.float)
94+
else:
8195
lengths = np.array(lengths, dtype=np.float)
8296
assert lengths.shape == (3, )
83-
self._maxs += 0.5*lengths - 0.5*self.lengths
84-
self._mins -= 0.5*lengths - 0.5*self.lengths
85-
self._lengths = lengths
97+
assert all(lengths >= 0), "Given lengths are negative"
98+
self._maxs = _BoxArray(array=(self.maxs + (0.5*lengths - 0.5*self.lengths)), var="maxs", box=self)
99+
self._mins = _BoxArray(array=(self.mins - (0.5*lengths - 0.5*self.lengths)), var="mins", box=self)
100+
self._lengths = _BoxArray(array=lengths, var="lengths", box=self, dtype=np.float)
86101

87102
@angles.setter
88103
def angles(self, angles):
89-
if isinstance(angles, list):
90-
angles = np.array(angles, dtype=np.float)
104+
angles = np.array(angles, dtype=np.float)
91105
assert angles.shape == (3, )
92-
self._angles = angles
106+
self._angles = _BoxArray(array=angles, var="angles", box=self, dtype=np.float)
93107

94108
def __repr__(self):
95109
return "Box(mins={}, maxs={}, angles={})".format(self.mins, self.maxs, self.angles)
110+
111+
class _BoxArray(np.ndarray):
112+
"""Subclass of np.ndarry specifically for mb.Box
113+
114+
This subclass is meant to be used internally to store Box attribute array.
115+
This subclass is modified so that its __setitem__ method is reroute to the
116+
corresponding setter method.
117+
118+
Parameters
119+
----------
120+
array : array-like object
121+
This can be tuple, list, or any array-like object that can be usually
122+
passed to np.array
123+
var : str
124+
Corresponding Box's attributes like "maxs", "mins", "lengths", "angles"
125+
box : mb.Box
126+
This is the Box contains this attribute (one level up of this array)
127+
"""
128+
def __new__(cls, array, var=None, box=None, dtype=np.float):
129+
_array = np.asarray(array, dtype).view(cls)
130+
_array.var = var
131+
_array.box = box
132+
return _array
133+
134+
def __setitem__(self, key, val):
135+
array = list(self)
136+
array[key] = val
137+
if self.var == "maxs":
138+
msg = "Given max value is less than box's min value"
139+
assert val >= self.box.mins[key], msg
140+
self.box.maxs = array
141+
elif self.var == "mins":
142+
msg = "Given min value is more than box's max value"
143+
assert val <= self.box.maxs[key], msg
144+
self.box.mins = array
145+
elif self.var == "lengths":
146+
msg = "Given length value is negative"
147+
assert val >= 0, msg
148+
self.box.lengths = array
149+
else:
150+
self.box.angles = array

mbuild/formats/lammpsdata.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ def write_lammpsdata(structure, filename, atom_style='full',
142142
box = Box(lengths=np.array([0.1 * val for val in structure.box[0:3]]),
143143
angles=structure.box[3:6])
144144
# Divide by conversion factor
145-
box.lengths /= sigma_conversion_factor
146145
box.maxs /= sigma_conversion_factor
147146

148147
# Lammps syntax depends on the functional form

mbuild/tests/test_box.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,44 @@ def test_setters_with_lists(self):
7272
assert (box.maxs - box.mins == 4 * np.ones(3)).all()
7373
box.angles = [90, 90, 120]
7474
assert (box.angles == np.array([90, 90, 120])).all()
75+
76+
def test_single_dimension_setter(self):
77+
box = mb.Box(mins=np.zeros(3), maxs=4*np.ones(3))
78+
assert (box.lengths == 4*np.ones(3)).all()
79+
80+
box.maxs[0] = 5
81+
box.mins[2] = 1
82+
assert np.allclose(box.mins, np.array([0, 0, 1], dtype=np.float))
83+
assert np.allclose(box.maxs, np.array([5, 4, 4], dtype=np.float))
84+
assert np.allclose(box.lengths, np.array([5, 4, 3], dtype=np.float))
85+
86+
box.lengths[1] = 6
87+
assert np.allclose(box.mins, np.array([0, -1, 1], dtype=np.float))
88+
assert np.allclose(box.maxs, np.array([5, 5, 4], dtype=np.float))
89+
assert np.allclose(box.lengths, np.array([5, 6, 3], dtype=np.float))
90+
91+
new_box = mb.Box(5)
92+
assert np.allclose(new_box.lengths, np.array([5, 5, 5], dtype=np.float))
93+
assert np.allclose(new_box.mins, np.array([0, 0, 0], dtype=np.float))
94+
assert np.allclose(new_box.maxs, np.array([5, 5, 5], dtype=np.float))
95+
96+
new_box.lengths = 4
97+
assert np.allclose(new_box.lengths, np.array([4, 4, 4], dtype=np.float))
98+
assert np.allclose(new_box.mins, np.array([0.5, 0.5, 0.5], dtype=np.float))
99+
assert np.allclose(new_box.maxs, np.array([4.5, 4.5, 4.5], dtype=np.float))
100+
101+
def test_sanity_checks(self):
102+
# Initialization step
103+
with pytest.raises(AssertionError):
104+
box = mb.Box(mins=[3,3,3], maxs=[1,1,1])
105+
with pytest.raises(AssertionError):
106+
box = mb.Box(lengths=-1)
107+
108+
# Modifying step
109+
box = mb.Box(mins=[2,2,2], maxs=[4,4,4])
110+
with pytest.raises(AssertionError):
111+
box.mins[1] = box.maxs[1] + 1
112+
with pytest.raises(AssertionError):
113+
box.maxs[1] = box.mins[1] - 1
114+
with pytest.raises(AssertionError):
115+
box.lengths = -1

0 commit comments

Comments
 (0)