Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

default mass and charge of Compound to None #1047

Merged
merged 10 commits into from
Aug 8, 2022
36 changes: 26 additions & 10 deletions mbuild/compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ class Compound(object):
The type of Compound.
pos : np.ndarray, shape=(3,), dtype=float, optional, default=[0, 0, 0]
The position of the Compound in Cartestian space
mass : float, optional, default=0.0
mass : float, optional, default=None
The mass of the compound. If none is set, then will try to
infer the mass from a compound's element attribute.
If neither `mass` or `element` are specified, then the
mass will be zero.
mass will be None.
charge : float, optional, default=0.0
Currently not used. Likely removed in next release.
periodicity : tuple of bools, length=3, optional, default=None
Expand Down Expand Up @@ -147,8 +147,8 @@ def __init__(
subcompounds=None,
name=None,
pos=None,
mass=0.0,
charge=0.0,
mass=None,
charge=None,
periodicity=None,
box=None,
element=None,
Expand Down Expand Up @@ -202,7 +202,7 @@ def __init__(
raise MBuildError(
"Can't set the mass of a Compound with subcompounds. "
)
self._charge = 0.0
self._charge = None
self._mass = mass
self.add(subcompounds)
else:
Expand Down Expand Up @@ -358,17 +358,26 @@ def mass(self):
if self._contains_only_ports():
return self._particle_mass(self)
else:
return sum([self._particle_mass(p) for p in self.particles()])
particle_masses = [self._particle_mass(p) for p in self.particles()]
if None in particle_masses:
warn(
f"Some particle of {self} does not have mass."
"They will not be accounted for during this calculation."
)
filtered_masses = [
mass for mass in particle_masses if mass is not None
]
return sum(filtered_masses) if filtered_masses else None

@staticmethod
def _particle_mass(particle):
if particle._mass:
if particle._mass is not None:
return particle._mass
else:
if particle.element:
return particle.element.mass
else:
return 0
return None

@mass.setter
def mass(self, value):
Expand All @@ -386,7 +395,14 @@ def mass(self, value):
@property
def charge(self):
"""Get the charge of the Compound."""
return sum([particle._charge for particle in self.particles()])
charges = [p._charge for p in self.particles()]
if None in charges:
warn(
f"Some particle of {self} does not have a charge."
"They will not be accounted for during this calculation."
)
filtered_charges = [charge for charge in charges if charge is not None]
return sum(filtered_charges) if filtered_charges else None

@charge.setter
def charge(self, value):
Expand Down Expand Up @@ -673,7 +689,7 @@ def add(
"Only objects that inherit from mbuild.Compound can be added "
f"to Compounds. You tried to add '{new_child}'."
)
if self._mass != 0.0 and not isinstance(new_child, Port):
if self._mass is not None and not isinstance(new_child, Port):
warn(
f"{self} has a pre-defined mass of {self._mass}, "
"which will be reset to zero now that it contains children "
Expand Down
2 changes: 1 addition & 1 deletion mbuild/formats/protobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def _mb_to_proto(cmpd, proto):
"""Given mb.Compound, parse propertes into compound_pb2.Compound."""
proto.name = cmpd.name
proto.pos.x, proto.pos.y, proto.pos.z = cmpd.pos
proto.charge = cmpd.charge
proto.charge = cmpd.charge if cmpd.charge else 0.0
proto.id = id(cmpd)
(
proto.periodicity.x,
Expand Down
7 changes: 4 additions & 3 deletions mbuild/packing.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,9 +772,10 @@ def _validate_mass(compound, n_compounds):
found_zero_mass = False
total_mass = 0
for c, n in zip(compound, n_compounds):
comp_masses = [c._particle_mass(p) for p in c.particles()]
if 0.0 in comp_masses:
comp_masses = np.array([c._particle_mass(p) for p in c.particles()])
if 0.0 in comp_masses or None in comp_masses:
found_zero_mass = True
comp_masses[comp_masses == None] = 0.0
total_mass += np.sum(comp_masses) * n

if total_mass == 0:
Expand All @@ -788,7 +789,7 @@ def _validate_mass(compound, n_compounds):
if found_zero_mass:
warnings.warn(
"Some of the compounds or subcompounds in `compound` "
"have a mass of zero. This may have an effect on "
"have a mass of zero/None. This may have an effect on "
"density calculations"
)
return total_mass
Expand Down
34 changes: 32 additions & 2 deletions mbuild/tests/test_compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ def test_init_mass(self):
assert bead_overwrite.mass == 1.0

bead_no_mass = mb.Compound(name="A")
assert bead_no_mass.mass == 0.0
assert bead_no_mass.mass == None

def test_init_with_bad_mass(self):
with pytest.raises(MBuildError):
Expand Down Expand Up @@ -462,6 +462,22 @@ def test_mass_add_port(self):
A.add(mb.Port())
assert A.mass == 2.0

def test_none_mass(self):
A = mb.Compound()
assert A.mass == None

container = mb.Compound(subcompounds=[A])
with pytest.warns(UserWarning):
container_mass = container.mass
assert container_mass == None

A.mass = 1
B = mb.Compound()
container.add(B)
with pytest.warns(UserWarning):
container_mass = container.mass
assert container_mass == A.mass == 1

def test_add_existing_parent(self, ethane, h2o):
water_in_water = mb.clone(h2o)
h2o.add(water_in_water)
Expand Down Expand Up @@ -1250,7 +1266,7 @@ def test_charge(self, ch2, ch3):
compound = Compound(charge=2.0)
assert compound.charge == 2.0
compound2 = Compound()
assert compound2.charge == 0.0
assert compound2.charge == None

ch2[0].charge = 0.5
ch2[1].charge = -0.25
Expand All @@ -1277,6 +1293,20 @@ def test_charge_neutrality_warn(self, benzene):
with pytest.warns(UserWarning):
benzene.save("charge-test.mol2")

def test_none_charge(self):
A = mb.Compound()
with pytest.warns(UserWarning):
A.charge

A.charge = 1
B = mb.Compound()
container = mb.Compound(subcompounds=[A, B])
with pytest.warns(UserWarning):
container_charge = container.charge
assert A.charge == 1
assert B.charge == None
assert container_charge == 1

@pytest.mark.skipif(
not has_openbabel, reason="Open Babel package not installed"
)
Expand Down