Skip to content

Commit bf21ba0

Browse files
authored
fix: fix .show() crash when restraints weren't added with add_soft_bounds (#227)
1 parent 0f518f5 commit bf21ba0

8 files changed

Lines changed: 168 additions & 14 deletions

File tree

‎news/fix-show-restraints.rst‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* <news item>
4+
5+
**Changed:**
6+
7+
* <news item>
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* ``show()`` no longer fails when the recipe holds BVS or ObjCryst molecule restraints, or a ``Restraint`` added with ``register_soft_bounds``.
20+
21+
**Security:**
22+
23+
* <news item>

‎src/diffpy/srfit/fitbase/recipeorganizer.py‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,20 +1681,7 @@ def _format_restraints(self):
16811681
The list of formatted lines displaying the defined
16821682
restraints. Empty list when no restraints were defined.
16831683
"""
1684-
rset = self._get_restraints()
1685-
rlines = []
1686-
for res in rset:
1687-
line = (
1688-
"%s: lower_bound = %f, upper_bound = %f, sig = %f, scaled = %s"
1689-
% (
1690-
res.eqstr,
1691-
res.lower_bound,
1692-
res.upper_bound,
1693-
res.sig,
1694-
res.scaled,
1695-
)
1696-
)
1697-
rlines.append(line)
1684+
rlines = [str(restraint) for restraint in self._get_restraints()]
16981685
rlines.sort(key=numstr)
16991686
return rlines
17001687

‎src/diffpy/srfit/fitbase/restraint.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ class Restraint(Validatable):
4141
eq : Equation
4242
The equation whose evaluation is compared against the restraint
4343
bounds.
44+
eqstr : str
45+
The text shown for the restrained equation. It is the name of
46+
``eq`` unless set otherwise, as ``add_soft_bounds`` does.
4447
lower_bound : float
4548
The lower bound on the restraint evaluation (default -inf).
4649
upper_bound : float
@@ -75,12 +78,21 @@ def __init__(
7578
(default False).
7679
"""
7780
self.eq = eq
81+
self.eqstr = eq.name
7882
self.lower_bound = float(lower_bound)
7983
self.upper_bound = float(upper_bound)
8084
self.sig = float(sig)
8185
self.scaled = bool(scaled)
8286
return
8387

88+
def __str__(self):
89+
"""Return the equation with its bounds and settings."""
90+
return (
91+
f"{self.eqstr}: lower_bound = {self.lower_bound:f}, "
92+
f"upper_bound = {self.upper_bound:f}, sig = {self.sig:f}, "
93+
f"scaled = {self.scaled}"
94+
)
95+
8496
def penalty(self, w=1.0):
8597
"""Calculate the penalty of the restraint.
8698

‎src/diffpy/srfit/structure/bvsrestraint.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ def __init__(self, parset, sig=1, scaled=False):
6666
self.scaled = bool(scaled)
6767
return
6868

69+
def __str__(self):
70+
"""Return the structure name with the restraint settings."""
71+
return (
72+
f"BVS of {self._parset.name}: sig = {self.sig:f}, "
73+
f"scaled = {self.scaled}"
74+
)
75+
6976
def penalty(self, w=1.0):
7077
"""Calculate the penalty of the restraint.
7178

‎src/diffpy/srfit/structure/objcrystparset.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,24 @@ def __init__(self, res, scaled=False):
898898
self.scaled = scaled
899899
return
900900

901+
def __str__(self):
902+
"""Return the restrained atoms with the restraint settings."""
903+
res = self.res
904+
if hasattr(res, "GetLength0"):
905+
target = (
906+
f"length = {res.GetLength0():f}, "
907+
f"sigma = {res.GetLengthSigma():f}, "
908+
f"delta = {res.GetLengthDelta():f}"
909+
)
910+
else:
911+
target = (
912+
f"angle = {res.GetAngle0():f}, "
913+
f"sigma = {res.GetAngleSigma():f}, "
914+
f"delta = {res.GetAngleDelta():f}"
915+
)
916+
restraint_str = f"{res.GetName()}: {target}, scaled = {self.scaled}"
917+
return restraint_str
918+
901919
def penalty(self, w=1.0):
902920
"""Calculate the penalty of the restraint.
903921

‎tests/test_diffpyparset.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import unittest
1919

2020
import numpy as np
21+
import pytest
2122

2223
from diffpy.srfit.structure.diffpyparset import DiffpyStructureParSet
2324

@@ -127,6 +128,27 @@ def test_pickling():
127128
return
128129

129130

131+
def test_show_bvs_restraint(capturestdout, diffpy_srreal_available):
132+
# C1: A scaled BVS restraint with sig = 0.5 is added to the structure
133+
# nacl.
134+
# Expected: The Restraints section lists it as the BVS of nacl with its
135+
# sig and scaling.
136+
if not diffpy_srreal_available:
137+
pytest.skip("diffpy.srreal package not available")
138+
from diffpy.structure import Atom, Lattice, Structure
139+
140+
lattice = Lattice(5.6, 5.6, 5.6, 90, 90, 90)
141+
atoms = [Atom("Na", [0, 0, 0]), Atom("Cl", [0.5, 0.5, 0.5])]
142+
parameter_set = DiffpyStructureParSet(
143+
"nacl", Structure(atoms, lattice=lattice)
144+
)
145+
parameter_set.restrainBVS(sig=0.5, scaled=True)
146+
lines = capturestdout(parameter_set.show).splitlines()
147+
actual_restraint_lines = lines[lines.index("Restraints") + 2 :]
148+
expected_restraint_lines = ["BVS of nacl: sig = 0.500000, scaled = True"]
149+
assert actual_restraint_lines == expected_restraint_lines
150+
151+
130152
# End of class TestParameterAdapter
131153

132154
if __name__ == "__main__":

‎tests/test_objcrystparset.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,57 @@ def testExplicitBondLengthRestraints(self):
318318

319319
return
320320

321+
def test_show_explicit_molecule_restraints(self, capturestdout):
322+
# C1: A bond length, a bond angle and a dihedral angle restraint are
323+
# added through the ObjCrystMoleculeParSet.
324+
# Expected: The Restraints section lists each restraint by its atom
325+
# names with its target value, sigma, delta and scaling.
326+
cryst = ObjCrystCrystalParSet("bucky", self.occryst)
327+
molecule = cryst.c60
328+
atoms = molecule.atoms
329+
molecule.restrainBondLength(atoms[0], atoms[5], 3.3, 0.1, 0.2)
330+
molecule.restrainBondAngle(atoms[0], atoms[5], atoms[7], 1.0, 0.1, 0.2)
331+
molecule.restrainDihedralAngle(
332+
atoms[0], atoms[5], atoms[7], atoms[8], 2.0, 0.1, 0.2
333+
)
334+
lines = capturestdout(cryst.show, textwidth=0).splitlines()
335+
actual_restraint_lines = set(lines[lines.index("Restraints") + 2 :])
336+
expected_restraint_lines = {
337+
"C0-C5: length = 3.300000, sigma = 0.100000, delta = 0.200000, "
338+
"scaled = False",
339+
"C0-C5-C7: angle = 1.000000, sigma = 0.100000, delta = 0.200000, "
340+
"scaled = False",
341+
"C0-C5-C7-C8: angle = 2.000000, sigma = 0.100000, "
342+
"delta = 0.200000, scaled = False",
343+
}
344+
assert actual_restraint_lines == expected_restraint_lines
345+
346+
def test_show_implicit_molecule_restraints(self, capturestdout):
347+
# C1: A bond length, a bond angle and a dihedral angle restraint are
348+
# defined on the pyobjcryst Molecule and wrapped with
349+
# wrapRestraints.
350+
# Expected: The Restraints section lists each restraint by its atom
351+
# names with its target value, sigma, delta and scaling.
352+
ocmol = self.ocmol
353+
ocmol.AddBond(ocmol[0], ocmol[5], 3.3, 0.1, 0.2)
354+
ocmol.AddBondAngle(ocmol[0], ocmol[5], ocmol[7], 1.0, 0.1, 0.2)
355+
ocmol.AddDihedralAngle(
356+
ocmol[0], ocmol[5], ocmol[7], ocmol[8], 2.0, 0.1, 0.2
357+
)
358+
cryst = ObjCrystCrystalParSet("bucky", self.occryst)
359+
cryst.c60.wrapRestraints()
360+
lines = capturestdout(cryst.show, textwidth=0).splitlines()
361+
actual_restraint_lines = set(lines[lines.index("Restraints") + 2 :])
362+
expected_restraint_lines = {
363+
"C0-C5: length = 3.300000, sigma = 0.100000, delta = 0.200000, "
364+
"scaled = False",
365+
"C0-C5-C7: angle = 1.000000, sigma = 0.100000, delta = 0.200000, "
366+
"scaled = False",
367+
"C0-C5-C7-C8: angle = 2.000000, sigma = 0.100000, "
368+
"delta = 0.200000, scaled = False",
369+
}
370+
assert actual_restraint_lines == expected_restraint_lines
371+
321372
def testExplicitBondAngleRestraints(self):
322373
"""Test the structure with explicit bond angles.
323374

‎tests/test_recipeorganizer.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
import pytest
2222

2323
from diffpy.srfit import Calculator
24+
from diffpy.srfit.equation import Equation
2425
from diffpy.srfit.equation.builder import EquationFactory
2526
from diffpy.srfit.fitbase.parameter import Parameter
2627
from diffpy.srfit.fitbase.recipeorganizer import (
2728
RecipeContainer,
2829
RecipeOrganizer,
2930
get_equation_from_string,
3031
)
32+
from diffpy.srfit.fitbase.restraint import Restraint
3133
from diffpy.utils._deprecator import deprecated
3234

3335

@@ -901,6 +903,38 @@ def test_show_pattern(capturestdout, input_stage, input_pattern):
901903
assert actual_output == expected_output
902904

903905

906+
def test_show_added_soft_bounds(capturestdout):
907+
# C1: The soft bound 0 < p < 2 is added with add_soft_bounds.
908+
# Expected: The Restraints section lists it by the Parameter name p.
909+
organizer = _make_organizer()
910+
organizer._new_parameter("p", 1)
911+
organizer.add_soft_bounds("p", lower_bound=0, upper_bound=2)
912+
lines = capturestdout(organizer.show, textwidth=0).splitlines()
913+
actual_restraint_lines = lines[lines.index("Restraints") + 2 :]
914+
expected_restraint_lines = [
915+
"p: lower_bound = 0.000000, upper_bound = 2.000000, "
916+
"sig = 1.000000, scaled = False"
917+
]
918+
assert actual_restraint_lines == expected_restraint_lines
919+
920+
921+
def test_show_registered_soft_bounds(capturestdout):
922+
# C1: A Restraint 0 < p < 2 made directly from an Equation on p is
923+
# added with register_soft_bounds.
924+
# Expected: The Restraints section lists it by its equation name eq_p.
925+
organizer = _make_organizer()
926+
parameter = organizer._new_parameter("p", 1)
927+
restraint = Restraint(Equation(root=parameter), 0, 2)
928+
organizer.register_soft_bounds(restraint)
929+
lines = capturestdout(organizer.show, textwidth=0).splitlines()
930+
actual_restraint_lines = lines[lines.index("Restraints") + 2 :]
931+
expected_restraint_lines = [
932+
"eq_p: lower_bound = 0.000000, upper_bound = 2.000000, "
933+
"sig = 1.000000, scaled = False"
934+
]
935+
assert actual_restraint_lines == expected_restraint_lines
936+
937+
904938
def _make_iterpars_tree():
905939
"""Build a small hierarchy for iterate_over_parameters tests."""
906940
root = RecipeContainer("root")

0 commit comments

Comments
 (0)