Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions ThermoScreening/thermo/thermo.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ def run(self):
-------
None
"""
# Disabled: _transform_units is the only method that mutates the shared
# System in place (frequencies/energy). temperature_scan relies on run()
# leaving the System untouched, so keep this off (or make it copy first).
# self._transform_units()
self._rotational_contribution()
self._vibrational_contribution()
Expand All @@ -142,6 +145,41 @@ def run(self):
self._summary()


def temperature_scan(self, temperatures):
"""
Recompute the thermochemistry at each of ``temperatures`` (in Kelvin).

The electronic energy, geometry, and vibrational frequencies are
temperature-independent, so this reuses the same :class:`System` (and
this object's pressure, engine, and quasi-RRHO setting) and only the
thermal terms are recomputed -- i.e. a temperature scan from a single
Hessian. This object is left unchanged.

Parameters
----------
temperatures : iterable of float
Temperatures in Kelvin.

Returns
-------
list of Thermo
A ``Thermo`` computed (``run()`` already called) for each
temperature, in the given order.
"""
scan = []
for temperature in temperatures:
thermo = Thermo(
temperature=temperature,
pressure=self._pressure,
system=self._system,
engine=self._engine,
quasi_rrho=self._quasi_rrho,
)
thermo.run()
scan.append(thermo)
return scan


def _transform_units(self):
"""
Transforms the units of the system to the correct units.
Expand Down
3 changes: 2 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ Thermochemistry core

.. autoclass:: ThermoScreening.thermo.thermo.Thermo
:members: total_energy, total_enthalpy, total_gibbs_free_energy,
total_entropy, total_heat_capacity, total_EeGtot, electronic_energy
total_entropy, total_heat_capacity, total_EeGtot, electronic_energy,
temperature_scan

Coordinate and frequency readers
--------------------------------
Expand Down
16 changes: 16 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ The ensemble free energy lies at or below the lowest conformer's by the mixing
(conformational) entropy; use ``lowest_gibbs`` when you instead want the single
dominant structure to carry forward (e.g. into :func:`reaction_free_energy`).

Temperature scans
-----------------

The electronic energy, geometry and frequencies do not depend on temperature,
so a single (expensive) DFTB+/Hessian run can be reused across many
temperatures:

.. code-block:: python

thermo = dftbplus_thermo(molecule("H2O"), **dftb_3ob_parameters)
scan = thermo.temperature_scan([250, 273.15, 298.15, 350, 400])
gibbs = [(t._temperature, t.total_gibbs_free_energy("H")) for t in scan]

Each entry is a fully-evaluated ``Thermo`` at that temperature; the original
object is left unchanged.

Reactions and redox
-------------------

Expand Down
24 changes: 24 additions & 0 deletions tests/thermo/test_thermo.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,30 @@ def test_thermo_rejects_negative_pressure():
Thermo(temperature=298.15, pressure=-1.0, system=_valid_system(), engine="dftb+")


def test_temperature_scan_reuses_system_and_varies_with_temperature():
system = _valid_system()
thermo = Thermo(temperature=298.15, pressure=101325, system=system, engine="dftb+")

temperatures = [250.0, 298.15, 350.0]
scan = thermo.temperature_scan(temperatures)

assert [t._temperature for t in scan] == temperatures
# the expensive System (geometry/frequencies) is reused, not rebuilt
assert all(t._system is system for t in scan)
# temperature actually changes the thermochemistry
gibbs = [t.total_gibbs_free_energy("H") for t in scan]
assert len(set(gibbs)) == 3
# the scan leaves this object untouched
assert thermo._temperature == 298.15


def test_temperature_scan_empty_and_invalid():
thermo = Thermo(temperature=298.15, pressure=101325, system=_valid_system(), engine="dftb+")
assert thermo.temperature_scan([]) == []
with pytest.raises(TSValueError, match="temperature is negative"):
thermo.temperature_scan([300.0, -5.0])


# --- geometry-dependent thermochemistry, validated against ASE IdealGasThermo --- #

from ase import Atoms # noqa: E402
Expand Down
Loading