This project is a transpilation of the Matlab project defined in this paper.
- Project Structure
- Dependencies
- ENV Vars
- Building the project
- Using the CLI
- Input files
- Defining a new model
The project is structured as follows:
deps/: Contains the dependencies of the project.modules/: Contains the different modules of the project.cli/: CLI that enables the use of the cuqdyn-c library.cuqdyn-c/: The main library that implements the functionality of the paper project.sacess/: External proyect adapted to enable the use of eSS in C.cuqdyn-rs/: Rust crate. Parses the model from the XML file to evaluate it.
tests/: Constains the tests of the cuqdyn-c library.CUQDyn/: Contains the original Matlab project.
- xml2-2.9.1
- cvodes-7.3.0 (Built by CMake)
- hdf5-1.8.12 (Built by CMake)
- gsl-2.7 (Built by CMake if not present)
When running this software, there is a chance of generating an inf or NaN value. Take for example the following ODE:
<ode_expr y_count="1" p_count="2">
p0 * y0 * (1 - y0 / p1)
</ode_expr>If the value of p1 generated by the eSS solver is 0, the model eval produces an inf value. To avoid this, when the evaluation of the ODE generates a non-finite value, the software will set it to 0. If you want to change this behavior, you can set the value you want the variable to take when an invalid value is generated by setting the environment variable CUQDYN_DEF_YDOT to the desired value.
Set this variable if you want to set the seed used by the eSS solver instead of generating a random one.
Set this variable if you want to set the minimum step used by the CVODES solver instead of the default value.
Set this variable if you want to set the maximum number of steps used by the CVODES solver instead of the default value.
GCC and GFortran v14 aren't supported by the project, so you need to use GCC v13 or lower.
The project has a scripts/build.sh script. build-[variant]/ directories will be created, each one representing a variant off the project builded. If you call the script without arguments, it will build all the variants, but you can also specify the variant you want, passing it as the first argument. The available variants are:
serial: Builds the project to only execute serial methods.mpi: Builds the project to execute the sequential solver in parallel using MPI.mpi2: Builds the project to only include the MPI and OpenMP defined in sacess-library.
After this, running scripts/test.sh is a good way to know if the cuqdyn library works as expected.
There is also a Dockerfile and a Docker Compose file to build and run the project in a container.
docker compose upAfter building using the scripts/build.sh script, you can execute this command to run the CLI with example config and data files like this:
mkdir output
VARIANT=serial
./build-${VARIANT}/modules/cli/cli solve \
-c example-files/lotka_volterra_cuqdyn_config.xml \
-s example-files/lotka_volterra_ess_${VARIANT}_config.xml \
-d example-files/lotka_volterra_paper_data.txt \
-o output/mkdir output
VARIANT=serial
./build-${VARIANT}/modules/cli/cli solve \
-c example-files/alpha_pinene_cuqdyn_config.xml \
-s example-files/alpha_pinene_ess_${VARIANT}_config.xml \
-d example-files/alpha_pinene_paper_data.txt \
-o output/mkdir output
VARIANT=serial
./build-${VARIANT}/modules/cli/cli solve \
-c example-files/logistic_model_cuqdyn_config.xml \
-s example-files/logistic_model_ess_${VARIANT}_config.xml \
-d example-files/logistic_model_paper_data.txt \
-o output/After this, the file output/cuqdyn-results.txt contains the results of the algorithm but reading it as a plain text is not very useful. To fix this, you can run: (Needs python3 and matplotlib installed)
python3 plot.py output/cuqdyn-results.txtNote: Be carefull when executing with mpirun, the number of precesses must be divisor of m - 1, where m is the number of rows in the input data matrix.
This will save a graphic representation for each y(t) in different png files inside the directory where the results are (output folder in this example).
To get information about all the options the cli supports, you can run the following command:
./build-{variant}/modules/cli/cli helpYou can also run
./build-{variant}/modules/cli/cli versionto get the version of the cuqdyn-c lib, sacess lib and cli you are using.
There are three types of input files that must be provided:
-
eSS Solver config xml: This file contains the configuration of the eSS solver used in the sacess library. The specifications of this xml and how to build it are in this link.
-
cuqdyn config xml: This file contains the configuration of the cuqdyn solver used in the cuqdyn-c library.
- tolerances: rtol and atol used by the cvodes library.
- ode_expr: ODE model expression or identifier.
- time_scaling: Scaling factor for time. Using lower than 1 helps cvodes speed. You may have to increase the number of sacess maxevals. The sacess lb, up, and point constraints also get scaled. The same for the params median written in the output file. Params that are dividing shouldn't be scaled as the testing shows but they get scaled as well. This helps the solver converge faster but hasn't been proved to be valid for all cases. This has been made with testing purposes, not recommended to use it.
- y0: Initial conditions of the ODE.
- states_transformer: Transformations expressions or identidier applied to the different states in case of the observed data being a combination of the different states.
There is an option to accelerate the process of evaluating the ODE by defining it and the states transformer inside the mevalexpr module. We will talk about this later.
<?xml version="1.0" encoding="UTF-8" ?> <cuqdyn-config> <tolerances> <rtol>1e-8</rtol> <atol>1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8, 1e-8</atol> </tolerances> <ode_expr y_count="15" p_count="29"> p19 - p20 * y0 - p16 * y0 p16 * y0 - p18 * y1 - p17 * y1 * y7 - p20 * y1 - p1 * y1 * y9 + p2 * y3 - p3 * y1 * y12 + p4 * y4 p18 * y1 + p17 * y1 * y7 - p20 * y2 p1 * y1 * y9 - p2 * y3 p3 * y1 * y12 - p4 * y4 p10 * y12 - p0 * y5 * y9 + p4 * y4 - p22 * y5 p22 * p21 * y5 - p0 * y10 * y6 p14 * y8 - p15 * y7 p12 + p11 * y6 - p13 * y8 -p1 * y1 * y9 - p0 * y9 * y5 + p8 * y11 - p9 * y9 - p24 * y9 + p25 * y10 -p0 * y10 * y6 + p24 * p21 * y9 - p25 * p21 * y10 p6 + p5 * y6 - p7 * y11 p0 * y9 * y5 - p10 * y12 - p3 * y1 * y12 + p23 * y13 p0 * y10 * y6 - p23 * p21 * y13 p27 + p26 * y6 - p28 * y14 </ode_expr> <time_scaling>0.001</time_scaling> <!-- Optional (Defaults to 1.0) --> <y0> <!-- Optional (Defaults to the first row of the data file) --> 0.200000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000316, 0.002296, 0.004783, 0.000003, 0.002507, 0.003436, 0.000003, 0.060000, 0.000079, 0.000003 </y0> <states_transformer count="6"> <!-- Optional --> y6 y9 + y12 y8 y0 + y1 + y2 y1 y11 </states_transformer> </cuqdyn-config>
-
Data file: The data file containing a mtrix of observed data and the initial value needed to solve the ODE. The data file should be a txt file written with the following format:
# The first row is gonna be used as the initial condition if y0 is not present in the config file 31 3 # Matrix dimensions so the parsing is easier 0 10 5 # Column 1: time, Column 2: y1(t), Column 3: y2(t) . . . 30 1e1 5e1
Using strings and evaluate them is slow compared to compiled instructions, so, to make this possible, we designed a way to define the models in Rust and compile them to machine code.
Let's dig into it with an example of the Lotka Volterra model:
First of all, we need to write some Rust code. We will be using the following file modules/cuqdyn-rs/src/models.rs. Inside, we create a new unit struct and implement the Model trait like this:
#[derive(Default)]
struct LotkaVolterra;
impl Model for LotkaVolterra {
fn eval(&self, _t: f64, y: &[f64], ydot: &mut [f64], p: &[f64]) {
ydot[0] = y[0] * (p[0] - p[1] * y[1]);
ydot[1] = - y[1] * (p[2] - p[3] * y[0]);
}
}Once the model is defined, we should give it an identifier:
pub fn eval_model_fun(model: &str, ode_expr: &OdeExpr) -> Box<dyn Model> {
match model {
"lotka-volterra" => Box::new(LotkaVolterra::default()),
"alpha-pinene" | "α-pinene" => Box::new(AlphaPinene::default()),
_ => Box::new(GenericModel::new(ode_expr))
}
}After you establish an identifier in the match at the bottom of the file, the model can be used indicating the identifier in the XML config file like this:
<?xml version="1.0" encoding="UTF-8" ?>
<cuqdyn-config>
<tolerances>
<rtol>1e-8</rtol>
<atol>1e-9, 1e-10</atol>
</tolerances>
<ode_expr y_count="2" p_count="4">
lotka-volterra
</ode_expr>
</cuqdyn-config>Note that y_count and p_count are still present.