Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable setting constraints on state variables #2340

Merged
merged 17 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
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_;
}

/**
Expand Down Expand Up @@ -277,6 +279,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 Serialize Solver (see boost::serialization::serialize)
* @param ar Archive to serialize to
Expand Down Expand Up @@ -1118,7 +1136,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 @@ -1636,6 +1654,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 @@ -1792,6 +1815,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 @@ -253,6 +253,8 @@ class CVodeSolver : public Solver {
void apply_max_nonlin_iters() const override;

void apply_max_conv_fails() const override;

void apply_constraints() const override;
};

} // namespace amici
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 @@ -233,6 +233,8 @@ class IDASolver : public Solver {
void apply_max_nonlin_iters() const override;

void apply_max_conv_fails() const override;

void apply_constraints() const override;
};

} // namespace amici
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
47 changes: 47 additions & 0 deletions python/tests/test_sbml_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,3 +722,50 @@ 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

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([1.0, 1.0])
Copy link
Member

Choose a reason for hiding this comment

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

can we add an enum for 0.0/1.0/2.0/-1.0/-2.0 values for a bit more user convenience??

Copy link
Member

Choose a reason for hiding this comment

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

also, probably makes sense to automatically add constraints when using Model::setStateIsNonNegative and add this behaviour to the documentation of setConstraints and setStateIsNonNegative.

Copy link
Member Author

@dweindl dweindl Mar 6, 2024

Choose a reason for hiding this comment

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

can we add an enum for 0.0/1.0/2.0/-1.0/-2.0 values for a bit more user convenience??

done

also, probably makes sense to automatically add constraints when using Model::setStateIsNonNegative and add this behaviour to the documentation of setConstraints and setStateIsNonNegative.

I am not completely sure about this one. While I agree that it makes sense to imply the constraints in setStateIsNonNegative True, this would lead to some funny behavior when disabling non-negativity.

# I want to enable constraints:
model.setConstraints([non_negative])
# I want to enable the max(0, state) in addition:
model.setStateIsNonNegative([True])
# I decide to disable the latter
model.setStateIsNonNegative([False]) # implies setConstraints([none]) ?!
# now it also cleared my original constraint

I think I'd prefer to keep them independent for now.

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 @@ -905,6 +905,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 @@ -1128,6 +1132,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 @@ -198,6 +198,8 @@

cpu_time_ = 0.0;
cpu_timeB_ = 0.0;

apply_constraints();
}

void Solver::setupB(
Expand Down Expand Up @@ -644,6 +646,15 @@
}
}

void Solver::apply_constraints() const {
if (constraints_.getLength() != 0
&& gsl::narrow<int>(constraints_.getLength()) != nx()) {
throw std::invalid_argument(

Check warning on line 652 in src/solver.cpp

View check run for this annotation

Codecov / codecov/patch

src/solver.cpp#L652

Added line #L652 was not covered by tests
"Constraints must have the same size as the state vector."
);

Check warning on line 654 in src/solver.cpp

View check run for this annotation

Codecov / codecov/patch

src/solver.cpp#L654

Added line #L654 was not covered by tests
}
}

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

SensitivityMethod Solver::getSensitivityMethodPreequilibration() const {
Expand Down Expand Up @@ -691,6 +702,21 @@

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);
}

int Solver::getNewtonMaxSteps() const { return newton_maxsteps_; }

void Solver::setNewtonMaxSteps(int const newton_maxsteps) {
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 @@
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");

Check warning on line 281 in src/solver_cvodes.cpp

View check run for this annotation

Codecov / codecov/patch

src/solver_cvodes.cpp#L281

Added line #L281 was not covered by tests
}
}

Solver* CVodeSolver::clone() const { return new CVodeSolver(*this); }

void CVodeSolver::allocateSolver() const {
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 @@
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");

Check warning on line 278 in src/solver_idas.cpp

View check run for this annotation

Codecov / codecov/patch

src/solver_idas.cpp#L278

Added line #L278 was not covered by tests
}
}

Solver* IDASolver::clone() const { return new IDASolver(*this); }

void IDASolver::allocateSolver() const {
Expand Down
Loading