Skip to content

Commit dded76b

Browse files
authored
Remove hessian conversion on normal mode calculation (#243)
* Ensure no unit conversion in hessian * Use a copy for value and valuearray * Raise exception on value `to` with inplace * Add missed changelog updates
1 parent 4276cab commit dded76b

File tree

7 files changed

+108
-40
lines changed

7 files changed

+108
-40
lines changed

autode/hessians.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ def _mass_weighted(self) -> np.ndarray:
234234
axis=np.newaxis,
235235
)
236236

237-
return Hessian(
238-
H / np.sqrt(np.outer(mass_array, mass_array)), units="J m^-2 kg^-1"
239-
)
237+
return np.array(
238+
H / np.sqrt(np.outer(mass_array, mass_array))
239+
) # J Å^-2 kg^-1
240240

241241
@cached_property
242242
def _proj_mass_weighted(self) -> np.ndarray:

autode/units.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,15 @@ def energy_unit_from_name(name: str) -> "autode.units.Unit":
211211

212212

213213
ha_per_ang = CompositeUnit(
214-
ha, per=[ang], aliases=["ha Å-1", "ha Å^-1", "ha/ang"]
214+
ha, per=[ang], aliases=["ha / Å", "ha Å-1", "ha Å^-1", "ha/ang"]
215215
)
216216

217217
ha_per_a0 = CompositeUnit(
218-
ha, per=[a0], aliases=["ha a0-1", "ha a0^-1", "ha/bohr"]
218+
ha, per=[a0], aliases=["ha / a0", "ha a0-1", "ha a0^-1", "ha/bohr"]
219219
)
220220

221221
ev_per_ang = CompositeUnit(
222-
ev, per=[ang], aliases=["ha a0-1", "ev Å^-1", "ev/ang"]
222+
ev, per=[ang], aliases=["ev / Å", "ev Å^-1", "ev/ang"]
223223
)
224224

225225
kcalmol_per_ang = CompositeUnit(

autode/values.py

+47-28
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@
3737
)
3838

3939

40-
def _to(value: Union["Value", "ValueArray"], units: Union[Unit, str]):
40+
def _to(
41+
value: Union["Value", "ValueArray"], units: Union[Unit, str], inplace: bool
42+
) -> Any:
4143
"""
42-
Convert a value or value array to a new unit and return a copy
44+
Convert a value or value array to a new unit and return a copy if
45+
inplace=False
4346
4447
---------------------------------------------------------------------------
4548
Arguments:
@@ -65,23 +68,26 @@ def _to(value: Union["Value", "ValueArray"], units: Union[Unit, str]):
6568
f"No viable unit conversion from {value.units} -> {units}"
6669
)
6770

68-
# Convert to the base unit, then to the new units
69-
c = float(units.conversion / value.units.conversion)
70-
71-
if isinstance(value, Value):
72-
return value.__class__(float(value) * c, units=units)
73-
74-
elif isinstance(value, ValueArray):
75-
value[:] = np.array(value, copy=True) * c
76-
value.units = units
77-
return value
78-
79-
else:
71+
if not (isinstance(value, Value) or isinstance(value, ValueArray)):
8072
raise ValueError(
8173
f"Cannot convert {value} to new units. Must be one of"
8274
f" Value of ValueArray"
8375
)
8476

77+
if isinstance(value, Value) and inplace:
78+
raise ValueError(
79+
"Cannot modify a value inplace as floats are immutable"
80+
)
81+
82+
# Convert to the base unit, then to the new units
83+
c = float(units.conversion / value.units.conversion)
84+
85+
new_value = value if inplace else value.copy()
86+
new_value *= c
87+
new_value.units = units
88+
89+
return None if inplace else new_value
90+
8591

8692
def _units_init(value, units: Union[Unit, str, None]):
8793
"""Initialise the units of this value
@@ -171,6 +177,11 @@ def _other_same_units(self, other):
171177

172178
return other.to(self.units)
173179

180+
def _like_self_from_float(self, value: float) -> "Value":
181+
new_value = self.__class__(value, units=self.units)
182+
new_value.__dict__.update(self.__dict__)
183+
return new_value
184+
174185
def __eq__(self, other) -> bool:
175186
"""Equality of two values, which may be in different units"""
176187

@@ -210,8 +221,8 @@ def __add__(self, other) -> "Value":
210221
if isinstance(other, np.ndarray):
211222
return other + float(self)
212223

213-
return self.__class__(
214-
float(self) + self._other_same_units(other), units=self.units
224+
return self._like_self_from_float(
225+
float(self) + self._other_same_units(other)
215226
)
216227

217228
def __mul__(self, other) -> Union[float, "Value"]:
@@ -225,8 +236,8 @@ def __mul__(self, other) -> Union[float, "Value"]:
225236
)
226237
return float(self) * self._other_same_units(other)
227238

228-
return self.__class__(
229-
float(self) * self._other_same_units(other), units=self.units
239+
return self._like_self_from_float(
240+
float(self) * self._other_same_units(other)
230241
)
231242

232243
def __rmul__(self, other) -> Union[float, "Value"]:
@@ -240,17 +251,11 @@ def __sub__(self, other) -> "Value":
240251

241252
def __floordiv__(self, other) -> Union[float, "Value"]:
242253
x = float(self) // self._other_same_units(other)
243-
if isinstance(other, Value):
244-
return x
245-
246-
return self.__class__(x, units=self.units)
254+
return x if isinstance(other, Value) else self._like_self_from_float(x)
247255

248256
def __truediv__(self, other) -> Union[float, "Value"]:
249257
x = float(self) / self._other_same_units(other)
250-
if isinstance(other, Value):
251-
return x
252-
253-
return self.__class__(x, units=self.units)
258+
return x if isinstance(other, Value) else self._like_self_from_float(x)
254259

255260
def __abs__(self) -> "Value":
256261
"""Absolute value"""
@@ -269,7 +274,7 @@ def to(self, units):
269274
Raises:
270275
(TypeError):
271276
"""
272-
return _to(self, units)
277+
return _to(self, units, inplace=False)
273278

274279

275280
class Energy(Value):
@@ -652,7 +657,21 @@ def to(self, units) -> Any:
652657
Raises:
653658
(TypeError):
654659
"""
655-
return _to(self, units)
660+
return _to(self, units, inplace=False)
661+
662+
def to_(self, units) -> None:
663+
"""
664+
Convert this array into a set of new units, inplace. This will not copy
665+
the array
666+
667+
-----------------------------------------------------------------------
668+
Returns:
669+
(None)
670+
671+
Raises:
672+
(TypeError):
673+
"""
674+
_to(self, units, inplace=True)
656675

657676
def __array_finalize__(self, obj):
658677
"""See https://numpy.org/doc/stable/user/basics.subclassing.html"""

doc/changelog.rst

+5-4
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ Changelog
88

99
Usability improvements/Changes
1010
******************************
11-
*
11+
- :code:`autode.value.ValueArray.to()` now defaults to copying the object rather than inplace modification
1212

1313

1414
Functionality improvements
1515
**************************
16-
-
16+
- Adds a :code:`to_` method to :code:`autode.value.ValueArray` for explicit inplace modification of the array
1717

1818

1919
Bug Fixes
2020
*********
2121
- Fixes :code:`ERROR` logging level being ignored from environment variable :code:`AUTODE_LOG_LEVEL`
2222
- Fixes :code:`autode.values.Value` instances generating items with units on division, and throw a warning if multiplying
2323
- Fixes the printing of cartesian constraints in XTB input files, meaning they are no longer ignored
24+
- Fixes :code:`Hessian` instances changing units when normal modes are calculated
25+
- Fixes an incorrect alias for :code:`ev_per_ang`
2426

2527

2628
1.3.4
@@ -32,7 +34,6 @@ Feature additions.
3234
Usability improvements/Changes
3335
******************************
3436
* Throw useful exception for invalid :code:`ade.Config.ts_template_folder_path`
35-
* Adds the reaction temperature to the unique reaction hash
3637

3738

3839
Functionality improvements
@@ -44,7 +45,7 @@ Functionality improvements
4445

4546
Bug Fixes
4647
*********
47-
-
48+
- Fixes calculation :code:`clean_up()` failing with a null filename
4849

4950

5051
1.3.3

tests/test_hessian.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from autode.species import Molecule
1414
from autode.values import Frequency
1515
from autode.geom import calc_rmsd
16-
from autode.units import wavenumber
16+
from autode.units import wavenumber, ha_per_ang_sq
1717
from autode.exceptions import CalculationException
1818
from autode.wrappers.keywords import pbe0
1919
from autode.transition_states.base import displaced_species_along_mode
@@ -243,6 +243,7 @@ def assert_correct_co2_frequencies(hessian, expected=(666, 1415, 2517)):
243243
"""Ensure the projected frequencies of CO2 are roughly right"""
244244
nu_1, nu_2, nu_3 = expected
245245

246+
print(hessian.frequencies_proj)
246247
assert sum(freq == 0.0 for freq in hessian.frequencies_proj) == 5
247248

248249
# Should have a degenerate bending mode for CO2 with ν = 666 cm-1
@@ -419,6 +420,7 @@ def test_hessian_modes():
419420

420421
h2o = Molecule("H2O_hess_orca.xyz")
421422
h2o.hessian = h2o_hessian_arr
423+
assert h2o.hessian.units == ha_per_ang_sq
422424

423425
# The structure is a minimum, thus there should be no imaginary frequencies
424426
assert h2o.imaginary_frequencies is None
@@ -441,6 +443,9 @@ def test_hessian_modes():
441443
vib_mode, h2o.hessian.normal_modes[6 + i], atol=0.1
442444
) or np.allclose(vib_mode, -h2o.hessian.normal_modes[6 + i], atol=0.1)
443445

446+
# Hessian units should be retained
447+
assert h2o.hessian.units == ha_per_ang_sq
448+
444449

445450
@testutils.work_in_zipped_dir(os.path.join(here, "data", "hessians.zip"))
446451
def test_proj_modes():
@@ -595,6 +600,8 @@ def test_nwchem_hessian_co2():
595600
keywords=ade.HessianKeywords(),
596601
)
597602
calc.set_output_filename("CO2_hess_nwchem.out")
603+
print(co2.hessian)
604+
print(co2.hessian._mass_weighted)
598605
assert_correct_co2_frequencies(
599606
hessian=co2.hessian, expected=(659.76, 1406.83, 2495.73)
600607
)

tests/test_value.py

+23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from autode.constants import Constants
44
from autode.units import ha, kjmol, kcalmol, ev, ang, a0, nm, pm, m, rad, deg
55
from autode.values import (
6+
_to,
67
Value,
78
Distance,
89
MWDistance,
@@ -278,3 +279,25 @@ def test_div_mul_generate_floats():
278279

279280
# Note: this behaviour is not ideal. But it is better than having the wrong units
280281
assert isinstance(e * e, float)
282+
283+
284+
def test_operations_maintain_other_attrs():
285+
286+
e = Energy(1, estimated=True, units="eV")
287+
assert e.is_estimated and e.units == ev
288+
289+
e *= 2
290+
assert e.is_estimated and e.units == ev
291+
292+
e /= 2
293+
assert e.is_estimated and e.units == ev
294+
295+
a = e * 2
296+
assert a.is_estimated and e.units == ev
297+
298+
299+
def test_inplace_value_modification_raises():
300+
301+
e = Energy(1, units="Ha")
302+
with pytest.raises(ValueError): # floats are immutable
303+
_to(e, units="eV", inplace=True)

tests/test_values.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,22 @@ class InvalidValue(float):
106106
def test_to_unsupported():
107107

108108
with pytest.raises(ValueError):
109-
_ = _to(InvalidValue(), Unit())
109+
_ = _to(InvalidValue(), Unit(), inplace=True)
110+
111+
112+
def test_inplace_modification():
113+
114+
x = Gradient([[1.0, 1.0, 1.0]], units="Ha / Å")
115+
return_value = x.to_("eV / Å")
116+
assert return_value is None
117+
118+
assert not np.allclose(x, np.ones(shape=(1, 3)))
119+
120+
121+
def test_copy_conversion():
122+
123+
x = Gradient([[1.0, 1.0, 1.0]], units="Ha / Å")
124+
y = x.to("eV / Å")
125+
126+
assert not np.allclose(x, y)
127+
assert np.allclose(x, np.ones(shape=(1, 3)))

0 commit comments

Comments
 (0)