Skip to content
Open
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
1 change: 1 addition & 0 deletions changes/user/bugfix.zero-temperature-rescaling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Zero-temperature rescaling remains finite and rejects positive targets that cannot be reached from zero kinetic energy.
26 changes: 25 additions & 1 deletion src/resetKinetics/resetKinetics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
#include <cstddef> // for size_t

#include "constants/conversionFactors.hpp" // for _FS_TO_S_, _S_TO_FS_
#include "exceptions.hpp" // for UserInputException
#include "mathUtilities.hpp" // for isZero
#include "physicalData.hpp" // for PhysicalData
#include "simulationBox.hpp" // for SimulationBox
#include "staticMatrix.hpp" // for operator*, operator+=
Expand All @@ -38,7 +40,9 @@ using namespace linearAlgebra;
using namespace physicalData;
using namespace simulationBox;
using namespace constants;
using namespace customException;
using namespace settings;
using namespace utilities;

/**
* @brief Construct a new Reset Kinetics:: Reset Kinetics object
Expand Down Expand Up @@ -127,7 +131,27 @@ void ResetKinetics::reset(
void ResetKinetics::resetTemperature(SimulationBox &simBox)
{
const auto targetTemp = ThermostatSettings::getActualTargetTemperature();
const auto lambda = ::sqrt(targetTemp / _temperature);

if (isZero(targetTemp))
{
std::ranges::for_each(
simBox.getAtoms(),
[](auto &atom) { atom->scaleVelocity(0.0); }
);

_temperature = simBox.calculateTemperature();
_momentum = simBox.calculateMomentum();
_angularMomentum = simBox.calculateAngularMomentum(_momentum);
return;
}

if (isZero(_temperature))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about updating this second if-statement condition to

if (isZero(_temperature) && !isZero(targetTemp))

then you could omit the first if all together

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That condition is correct for the error case, but the first branch cannot simply be removed: when both the current and target temperatures are zero, the remaining sqrt(targetTemp / _temperature) evaluates 0 / 0 and produces NaN. The explicit zero-target path also guarantees that all velocities are set exactly to zero before temperature, momentum, and angular momentum are recalculated. We would need a separate explicit zero/zero path even if the branches were consolidated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That condition is correct for the error case, but the first branch cannot simply be removed: when both the current and target temperatures are zero, the remaining sqrt(targetTemp / _temperature) evaluates 0 / 0 and produces NaN. The explicit zero-target path also guarantees that all velocities are set exactly to zero before temperature, momentum, and angular momentum are recalculated. We would need a separate explicit zero/zero path even if the branches were consolidated.

You are right, my proposal is not the solution, but what about this:
Leaving the first if, and re-writing the error message for the second if, then I think al 4 cases are correctly covered:
targetTemp is 0, _temperature is 0: error message
targetTemp is not 0, _temperature is 0: error message
targetTemp is 0, _temperature is not 0: lambda is correctly evaluated to 0
targetTemp is not 0, _temperature is not 0: lambda is correctly calculated

throw UserInputException(
"Cannot rescale a zero-temperature system to a positive target "
"temperature. Initialize velocities first."
);

const auto lambda = ::sqrt(targetTemp / _temperature);

std::ranges::for_each(
simBox.getAtoms(),
Expand Down
15 changes: 11 additions & 4 deletions src/thermostat/berendsenThermostat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@

#include <cmath> // for sqrt

#include "exceptions.hpp" // for UserInputException
#include "mathUtilities.hpp" // for isZero
#include "physicalData.hpp" // for PhysicalData
#include "simulationBox.hpp" // for SimulationBox
#include "thermostatSettings.hpp" // for ThermostatType
#include "timingsSettings.hpp" // for TimingsSettings

using thermostat::BerendsenThermostat;
using namespace customException;
using namespace settings;
using namespace simulationBox;
using namespace physicalData;
Expand Down Expand Up @@ -69,11 +71,16 @@ void BerendsenThermostat::applyThermostat(

_temperature = data.getTemperature();

// If the kinetic energy is (approximately) zero, there is nothing to
// thermostat: dividing by _temperature would NaN all velocities
// (1 / 0 -> Inf, then vel * Inf = NaN when vel is 0). Skip silently.
if (isZero(_temperature))
return;
{
if (isZero(_targetTemperature))
return;

throw UserInputException(
"Cannot apply Berendsen coupling to a zero-temperature system "
"with a positive target temperature. Initialize velocities first."
);
}

const auto dt = TimingsSettings::getTimeStep();
const auto tempRatio = _targetTemperature / _temperature;
Expand Down
15 changes: 15 additions & 0 deletions src/thermostat/velocityRescalingThermostat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,19 @@

#include <cmath> // for sqrt

#include "exceptions.hpp" // for UserInputException
#include "mathUtilities.hpp" // for isZero
#include "physicalData.hpp" // for PhysicalData
#include "simulationBox.hpp" // for SimulationBox
#include "thermostatSettings.hpp" // for ThermostatType
#include "timingsSettings.hpp" // for TimingsSettings

using thermostat::VelocityRescalingThermostat;
using namespace customException;
using namespace settings;
using namespace simulationBox;
using namespace physicalData;
using namespace utilities;

/**
* @brief Construct a new Velocity Rescaling Thermostat:: Velocity Rescaling
Expand Down Expand Up @@ -98,6 +102,17 @@ void VelocityRescalingThermostat::applyThermostat(

_temperature = physicalData.getTemperature();

if (isZero(_temperature))
{
if (isZero(_targetTemperature))
return;

throw UserInputException(
"Cannot apply velocity rescaling to a zero-temperature system "
"with a positive target temperature. Initialize velocities first."
);
}

const auto timeStep = TimingsSettings::getTimeStep();
const auto tempRatio = _targetTemperature / _temperature;
const auto dof = double(simulationBox.getDegreesOfFreedom());
Expand Down
39 changes: 39 additions & 0 deletions tests/src/resetKinetics/testResetKinetics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <memory>

#include "atom.hpp"
#include "exceptions.hpp"
#include "gtest/gtest.h"
#include "molecule.hpp"
#include "physicalData.hpp"
Expand Down Expand Up @@ -124,6 +125,44 @@ TEST(TestResetKinetics, resetTemperatureRescalesVelocitiesAndStaysFinite)
delete box;
}

TEST(TestResetKinetics, resetTemperatureSupportsZeroKelvin)
{
auto *box = makeBox();
resetKinetics::ResetKinetics resetKinetics;

for (const auto &atom : box->getAtoms()) atom->setVelocity({0.0, 0.0, 0.0});

settings::ThermostatSettings::setTargetTemperature(0.0);
resetKinetics.setTemperature(0.0);
resetKinetics.resetTemperature(*box);

auto data = physicalData::PhysicalData();
data.calculateTemperature(*box);
EXPECT_DOUBLE_EQ(data.getTemperature(), 0.0);
for (const auto &atom : box->getAtoms())
EXPECT_EQ(atom->getVelocity(), linearAlgebra::Vec3D(0.0, 0.0, 0.0));

delete box;
}

TEST(TestResetKinetics, rejectsPositiveTargetFromZeroTemperature)
{
auto *box = makeBox();
resetKinetics::ResetKinetics resetKinetics;

for (const auto &atom : box->getAtoms()) atom->setVelocity({0.0, 0.0, 0.0});

settings::ThermostatSettings::setTargetTemperature(300.0);
resetKinetics.setTemperature(0.0);

EXPECT_THROW(
resetKinetics.resetTemperature(*box),
customException::UserInputException
);

delete box;
}

TEST(TestResetKinetics, resetMomentumZerosTotalLinearMomentum)
{
auto *box = makeBox();
Expand Down
64 changes: 52 additions & 12 deletions tests/src/thermostat/testThermostat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#include "berendsenThermostat.hpp" // for BerendsenThermostat
#include "constants/internalConversionFactors.hpp" // for _TEMPERATURE_FACTOR_
#include "exceptions.hpp" // for UserInputException
#include "gtest/gtest.h" // for InitGoogleTest
#include "langevinThermostat.hpp" // for LangevinThermostat
#include "noseHooverThermostat.hpp" // for NoseHooverThermostat
Expand Down Expand Up @@ -197,29 +198,68 @@ TEST_F(TestThermostat, velocityRescaling_applyDoesNotNaN)
}
}

// Regression test: starting from zero kinetic energy (T == 0) used to
// produce NaN velocities, because tempRatio = T_target / 0 = Inf and
// the velocity scaling 0 * Inf = NaN. The guard skips the scaling and
// leaves velocities at zero.
TEST_F(TestThermostat, applyBerendsen_zeroTemperatureNoNaN)
TEST_F(TestThermostat, berendsenZeroTemperatureDoesNotNaN)
{
delete _thermostat;
_thermostat = new thermostat::BerendsenThermostat(0.0, 100.0);
settings::TimingsSettings::setTimeStep(0.1);

for (auto &atom : _simulationBox->getAtoms())
atom->setVelocity({0.0, 0.0, 0.0});

_thermostat->applyThermostat(*_simulationBox, *_data);

EXPECT_TRUE(std::isfinite(_data->getTemperature()));
for (const auto &atom : _simulationBox->getAtoms())
for (size_t dimension = 0; dimension < 3; ++dimension)
EXPECT_TRUE(std::isfinite(atom->getVelocity()[dimension]));
}

TEST_F(TestThermostat, berendsenRejectsPositiveTargetFromZero)
{
delete _thermostat;
_thermostat = new thermostat::BerendsenThermostat(300.0, 100.0);
settings::TimingsSettings::setTimeStep(0.1);

for (auto &atom : _simulationBox->getAtoms())
atom->setVelocity({0.0, 0.0, 0.0});

EXPECT_THROW(
_thermostat->applyThermostat(*_simulationBox, *_data),
customException::UserInputException
);
}

TEST_F(TestThermostat, velocityRescalingZeroTemperatureDoesNotNaN)
{
delete _thermostat;
_thermostat = new thermostat::VelocityRescalingThermostat(0.0, 100.0);
settings::TimingsSettings::setTimeStep(0.1);

for (auto &atom : _simulationBox->getAtoms())
atom->setVelocity({0.0, 0.0, 0.0});

_thermostat->applyThermostat(*_simulationBox, *_data);

EXPECT_FALSE(std::isnan(_data->getTemperature()));
EXPECT_FALSE(std::isinf(_data->getTemperature()));
EXPECT_TRUE(std::isfinite(_data->getTemperature()));
for (const auto &atom : _simulationBox->getAtoms())
for (size_t i = 0; i < 3; ++i)
{
EXPECT_FALSE(std::isnan(atom->getVelocity()[i]));
EXPECT_FALSE(std::isinf(atom->getVelocity()[i]));
}
for (size_t dimension = 0; dimension < 3; ++dimension)
EXPECT_TRUE(std::isfinite(atom->getVelocity()[dimension]));
}

TEST_F(TestThermostat, velocityRescalingRejectsPositiveTargetFromZero)
{
delete _thermostat;
_thermostat = new thermostat::VelocityRescalingThermostat(300.0, 100.0);
settings::TimingsSettings::setTimeStep(0.1);

for (auto &atom : _simulationBox->getAtoms())
atom->setVelocity({0.0, 0.0, 0.0});

EXPECT_THROW(
_thermostat->applyThermostat(*_simulationBox, *_data),
customException::UserInputException
);
}

/* ---------- LangevinThermostat ---------- */
Expand Down
Loading