Skip to content

Commit

Permalink
Enable setting constraints on state variables (AMICI-dev#2340)
Browse files Browse the repository at this point in the history
In addition to the current `Model.setStateIsNonNegative`, this adds the option to set additional
(non)negativity/positivity constraints to be enforced by the solver.

See [CVodeSetConstraints](https://sundials.readthedocs.io/en/latest/cvode/Usage/index.html#c.CVodeSetConstraints) for details.

Related to AMICI-dev#2327.
  • Loading branch information
dweindl authored Mar 7, 2024
1 parent 3dded5c commit 7a920e2
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 2 deletions.
9 changes: 9 additions & 0 deletions include/amici/defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ enum class SplineExtrapolation {
periodic = 3,
};

/** Constraints on state variables */
enum class Constraint {
none = 0,
non_negative = 1,
non_positive = -1,
positive = 2,
negative = -2,
};

// clang-format on

} // namespace amici
Expand Down
21 changes: 21 additions & 0 deletions include/amici/serialization.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "amici/rdata.h"
#include "amici/solver.h"
#include "amici/solver_cvodes.h"
#include "amici/vector.h"

#include <chrono>

Expand Down Expand Up @@ -87,6 +88,7 @@ void serialize(Archive& ar, amici::Solver& s, unsigned int const /*version*/) {
ar & s.maxtime_;
ar & s.max_conv_fails_;
ar & s.max_nonlin_iters_;
ar & s.constraints_;
ar & s.max_step_size_;
}

Expand Down Expand Up @@ -278,6 +280,25 @@ void serialize(
ar & m.ubw;
ar & m.lbw;
}

/**
* @brief Serialize AmiVector to a boost archive
* @param ar archive
* @param v AmiVector
*/
template <class Archive>
void serialize(
Archive& ar, amici::AmiVector& v, unsigned int const /*version*/
) {
if (Archive::is_loading::value) {
std::vector<realtype> tmp;
ar & tmp;
v = amici::AmiVector(tmp);
} else {
auto tmp = v.getVector();
ar & tmp;
}
}
#endif
} // namespace serialization
} // namespace boost
Expand Down
28 changes: 27 additions & 1 deletion include/amici/solver.h
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,24 @@ class Solver {
*/
int getMaxConvFails() const;

/**
* @brief Set constraints on the model state.
*
* See
* https://sundials.readthedocs.io/en/latest/cvode/Usage/index.html#c.CVodeSetConstraints.
*
* @param constraints
*/
void setConstraints(std::vector<realtype> const& constraints);

/**
* @brief Get constraints on the model state.
* @return constraints
*/
std::vector<realtype> getConstraints() const {
return constraints_.getVector();
}

/**
* @brief Set the maximum step size
* @param max_step_size maximum step size. `0.0` means no limit.
Expand Down Expand Up @@ -1130,7 +1148,7 @@ class Solver {
virtual void rootInit(int ne) const = 0;

/**
* @brief Initalize non-linear solver for sensitivities
* @brief Initialize non-linear solver for sensitivities
* @param model Model instance
*/
void initializeNonLinearSolverSens(Model const* model) const;
Expand Down Expand Up @@ -1648,6 +1666,11 @@ class Solver {
*/
void applySensitivityTolerances() const;

/**
* @brief Apply the constraints to the solver.
*/
virtual void apply_constraints() const;

/** pointer to solver memory block */
mutable std::unique_ptr<void, free_solver_ptr> solver_memory_;

Expand Down Expand Up @@ -1809,6 +1832,9 @@ class Solver {
/** flag indicating whether sensInit1 was called */
mutable bool sens_initialized_{false};

/** Vector of constraints on the solution */
mutable AmiVector constraints_;

private:
/**
* @brief applies total number of steps for next solver call
Expand Down
2 changes: 2 additions & 0 deletions include/amici/solver_cvodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ class CVodeSolver : public Solver {

void apply_max_conv_fails() const override;

void apply_constraints() const override;

void apply_max_step_size() const override;
};

Expand Down
2 changes: 2 additions & 0 deletions include/amici/solver_idas.h
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ class IDASolver : public Solver {

void apply_max_conv_fails() const override;

void apply_constraints() const override;

void apply_max_step_size() const override;
};

Expand Down
35 changes: 34 additions & 1 deletion include/amici/vector.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@

#include <gsl/gsl-lite.hpp>

namespace amici {
class AmiVector;
}

// for serialization friend
namespace boost {
namespace serialization {
template <class Archive>
void serialize(Archive& ar, amici::AmiVector& s, unsigned int version);
}
} // namespace boost

namespace amici {

/** Since const N_Vector is not what we want */
Expand Down Expand Up @@ -54,7 +66,7 @@ class AmiVector {
* @brief constructor from gsl::span,
* @param rvec vector from which the data will be copied
*/
explicit AmiVector(gsl::span<realtype> rvec)
explicit AmiVector(gsl::span<realtype const> rvec)
: AmiVector(std::vector<realtype>(rvec.begin(), rvec.end())) {}

/**
Expand Down Expand Up @@ -213,6 +225,17 @@ class AmiVector {
*/
void abs() { N_VAbs(getNVector(), getNVector()); };

/**
* @brief Serialize AmiVector (see boost::serialization::serialize)
* @param ar Archive to serialize to
* @param s Data to serialize
* @param version Version number
*/
template <class Archive>
friend void boost::serialization::serialize(
Archive& ar, AmiVector& s, unsigned int version
);

private:
/** main data storage */
std::vector<realtype> vec_;
Expand Down Expand Up @@ -405,6 +428,16 @@ namespace gsl {
inline span<realtype> make_span(N_Vector nv) {
return span<realtype>(N_VGetArrayPointer(nv), N_VGetLength_Serial(nv));
}

/**
* @brief Create span from N_Vector
* @param nv
*
*/
inline span<realtype const> make_span(amici::AmiVector const& av) {
return make_span(av.getVector());
}

} // namespace gsl

#endif /* AMICI_VECTOR_H */
2 changes: 2 additions & 0 deletions python/tests/test_hdf5.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def _modify_solver_attrs(solver):
elif attr == "setMaxTime":
# default value is the maximum, must not add to that
cval = random.random()
elif attr == "setConstraints":
cval = [1.0, 1.0]
elif isinstance(val, int):
cval = val + 1
else:
Expand Down
50 changes: 50 additions & 0 deletions python/tests/test_sbml_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,3 +722,53 @@ def test_hardcode_parameters(simple_sbml_model):
constant_parameters=["p1"],
hardcode_symbols=["p1"],
)


def test_constraints():
"""Test non-negativity constraint handling."""
from amici.antimony_import import antimony2amici
from amici import Constraint

ant_model = """
model test_non_negative_species
species A = 10
species B = 0
# R1: A => B; k1f * sqrt(A)
R1: A => B; k1f * max(0, A)
k1f = 1e10
end
"""
module_name = "test_non_negative_species"
with TemporaryDirectory(prefix=module_name) as outdir:
antimony2amici(
ant_model,
model_name=module_name,
output_dir=outdir,
compute_conservation_laws=False,
)
model_module = amici.import_model_module(
module_name=module_name, module_path=outdir
)
amici_model = model_module.getModel()
amici_model.setTimepoints(np.linspace(0, 100, 200))
amici_solver = amici_model.getSolver()
rdata = amici.runAmiciSimulation(amici_model, amici_solver)
assert rdata.status == amici.AMICI_SUCCESS
# should be non-negative in theory, but is expected to become negative
# in practice
assert np.any(rdata.x < 0)

amici_solver.setRelativeTolerance(1e-14)
amici_solver.setConstraints(
[Constraint.non_negative, Constraint.non_negative]
)
rdata = amici.runAmiciSimulation(amici_model, amici_solver)
assert rdata.status == amici.AMICI_SUCCESS
assert np.all(rdata.x >= 0)
assert np.all(
np.sum(rdata.x, axis=1) - np.sum(rdata.x[0])
< max(
np.sum(rdata.x[0]) * amici_solver.getRelativeTolerance(),
amici_solver.getAbsoluteTolerance(),
)
)
10 changes: 10 additions & 0 deletions src/hdf5.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,10 @@ void writeSolverSettingsToHDF5(
H5LTset_attribute_int(
file.getId(), hdf5Location.c_str(), "max_conv_fails", &ibuffer, 1
);

createAndWriteDouble1DDataset(
file, hdf5Location + "/constraints", solver.getConstraints()
);
}

void readSolverSettingsFromHDF5(
Expand Down Expand Up @@ -1143,6 +1147,12 @@ void readSolverSettingsFromHDF5(
getIntScalarAttribute(file, datasetPath, "max_conv_fails")
);
}

if (locationExists(file, datasetPath + "/constraints")) {
solver.setConstraints(
getDoubleDataset1D(file, datasetPath + "/constraints")
);
}
}

void readSolverSettingsFromHDF5(
Expand Down
26 changes: 26 additions & 0 deletions src/solver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ void Solver::setup(

cpu_time_ = 0.0;
cpu_timeB_ = 0.0;

apply_constraints();
}

void Solver::setupB(
Expand Down Expand Up @@ -646,6 +648,15 @@ void Solver::applySensitivityTolerances() const {
}
}

void Solver::apply_constraints() const {
if (constraints_.getLength() != 0
&& gsl::narrow<int>(constraints_.getLength()) != nx()) {
throw std::invalid_argument(
"Constraints must have the same size as the state vector."
);
}
}

SensitivityMethod Solver::getSensitivityMethod() const { return sensi_meth_; }

SensitivityMethod Solver::getSensitivityMethodPreequilibration() const {
Expand Down Expand Up @@ -693,6 +704,21 @@ void Solver::setMaxConvFails(int max_conv_fails) {

int Solver::getMaxConvFails() const { return max_conv_fails_; }

void Solver::setConstraints(std::vector<realtype> const& constraints) {
auto any_constraint
= std::any_of(constraints.begin(), constraints.end(), [](bool x) {
return x != 0.0;
});

if (!any_constraint) {
// all-0 must be converted to empty, otherwise sundials will fail
constraints_ = AmiVector();
return;
}

constraints_ = AmiVector(constraints);
}

void Solver::setMaxStepSize(realtype max_step_size) {
if (max_step_size < 0)
throw AmiException("max_step_size must be non-negative.");
Expand Down
12 changes: 12 additions & 0 deletions src/solver_cvodes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,18 @@ void CVodeSolver::apply_max_conv_fails() const {
throw CvodeException(status, "CVodeSetMaxConvFails");
}

void CVodeSolver::apply_constraints() const {
Solver::apply_constraints();

int status = CVodeSetConstraints(
solver_memory_.get(),
constraints_.getLength() > 0 ? constraints_.getNVector() : nullptr
);
if (status != CV_SUCCESS) {
throw CvodeException(status, "CVodeSetConstraints");
}
}

void CVodeSolver::apply_max_step_size() const {
int status = CVodeSetMaxStep(solver_memory_.get(), getMaxStepSize());
if (status != CV_SUCCESS)
Expand Down
12 changes: 12 additions & 0 deletions src/solver_idas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ void IDASolver::apply_max_conv_fails() const {
throw IDAException(status, "IDASetMaxConvFails");
}

void IDASolver::apply_constraints() const {
Solver::apply_constraints();

int status = IDASetConstraints(
solver_memory_.get(),
constraints_.getLength() > 0 ? constraints_.getNVector() : nullptr
);
if (status != IDA_SUCCESS) {
throw IDAException(status, "IDASetConstraints");
}
}

void IDASolver::apply_max_step_size() const {
int status = IDASetMaxStep(solver_memory_.get(), getMaxStepSize());
if (status != IDA_SUCCESS)
Expand Down
1 change: 1 addition & 0 deletions swig/amici.i
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ SteadyStateStatus = enum('SteadyStateStatus')
NewtonDampingFactorMode = enum('NewtonDampingFactorMode')
FixedParameterContext = enum('FixedParameterContext')
RDataReporting = enum('RDataReporting')
Constraint = enum('Constraint')
%}

%template(SteadyStateStatusVector) std::vector<amici::SteadyStateStatus>;
Expand Down

0 comments on commit 7a920e2

Please sign in to comment.