Created
July 21, 2024 10:17
-
-
Save mclements/ba5ce9964f838c25e07da13d487db0dd to your computer and use it in GitHub Desktop.
Extension to hesim for cCTSTMs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
Requires the transition_costs branch | |
Mark Clements | |
2024-07-08 | |
*/ | |
#include <boost/numeric/odeint.hpp> | |
#include <RcppArmadillo.h> | |
#include <hesim.h> | |
#include <hesim/ctstm/ctstm.h> | |
#include <hesim/statevals.h> | |
#include <hesim/dtstm.h> | |
// [[Rcpp::depends(RcppArmadillo)]] | |
// [[Rcpp::depends(hesim)]] | |
// [[Rcpp::depends(BH)]] | |
// Boilerplate code to ensure that arma plays nicely with boost::numeric::odeint | |
namespace boost { | |
namespace numeric { | |
namespace odeint { | |
template <> | |
struct is_resizeable<arma::vec> | |
{ | |
typedef boost::true_type type; | |
const static bool value = type::value; | |
}; | |
template <> | |
struct same_size_impl<arma::vec, arma::vec> | |
{ | |
static bool same_size(const arma::vec& x, const arma::vec& y) | |
{ | |
return x.size() == y.size(); | |
} | |
}; | |
template<> | |
struct resize_impl<arma::vec, arma::vec> | |
{ | |
static void resize(arma::vec& v1, const arma::vec& v2) | |
{ | |
v1.resize(v2.size()); | |
} | |
}; | |
} | |
} | |
} // namespace boost::numeric::odeint | |
#define CTSTM_OUT(OBJECT,COUNTER) \ | |
OBJECT.sample_[COUNTER] = s; \ | |
OBJECT.strategy_id_[COUNTER] = k; \ | |
OBJECT.patient_id_[COUNTER] = i; \ | |
OBJECT.grp_id_[COUNTER] = transmod->obs_index_.get_grp_id(); \ | |
OBJECT.patient_wt_[COUNTER] = transmod->obs_index_.get_patient_wt(); \ | |
OBJECT.state_id_[COUNTER] = h; | |
/***************************************************************************//** | |
* @ingroup ctstm | |
* Simulate disease progression (i.e., a path through a multi-state model). | |
* This function is exported to @c R and used in @c CohortCtstmTrans$sim_stateprobs() and | |
* in @c CohortCtstm$sim_disase(). | |
* @param R_CtstmTrans An R object of class @c CohortCtstmTrans. | |
* @param start_state The starting health state for each patient and random sample | |
* of the parameter sets. (TODO: allow for probabilities in the initial states.) | |
* @param start_age The starting age of each patient in the simulation. | |
* @param start_time The starting time of the simulation. | |
* @param max_t The maximum time to simulate the model for. | |
* @param max_age The maximum age that a patient can live to. | |
* @return An R data frame of the same format as stateprobs_out. | |
******************************************************************************/ | |
// [[Rcpp::export]] | |
Rcpp::List C_cohort_ctstm_sim(Rcpp::Environment R_CtstmTrans, | |
Rcpp::List R_CostsStateVals, | |
Rcpp::Environment R_QALYsStateVal, | |
std::vector<int> start_state, // by patient | |
std::vector<double> start_age, // by patient | |
std::vector<double> times, // common | |
std::string clock, | |
std::vector<int> transition_types, | |
int progress = 0, | |
double dr_qalys = 0.0, | |
double dr_costs = 0.0, | |
std::string type="predict", | |
double eps = 1e-100) { | |
using namespace boost::numeric::odeint; | |
typedef arma::vec state_type; | |
enum TransitionType {tt_time, tt_age}; | |
// Initialize | |
std::unique_ptr<hesim::ctstm::transmod> transmod = hesim::ctstm::transmod::create(R_CtstmTrans); | |
int n_costs = R_CostsStateVals.size(); | |
std::vector<hesim::statevals> costs_lookup; | |
std::vector<hesim::statmods::obs_index> obs_index_costs; | |
for (int cost_=0; cost_<n_costs; ++cost_) { | |
costs_lookup.push_back(hesim::statevals(Rcpp::as<Rcpp::Environment>(R_CostsStateVals[cost_]))); | |
obs_index_costs.push_back(hesim::statmods::obs_index(hesim::statmods::get_id_object(Rcpp::as<Rcpp::Environment>(R_CostsStateVals[cost_])))); | |
} | |
// use unique_ptr because statevals and obs_index can be null and do not have null constructors | |
std::unique_ptr<hesim::statevals> qalys_lookup; | |
std::unique_ptr<hesim::statmods::obs_index> obs_index_qalys; | |
bool valid_R_QALYsStateVal = R_QALYsStateVal.exists("method"); | |
if (valid_R_QALYsStateVal) { | |
qalys_lookup = std::unique_ptr<hesim::statevals>(new hesim::statevals(R_QALYsStateVal)); | |
auto id_object = hesim::statmods::get_id_object(R_QALYsStateVal); | |
obs_index_qalys = | |
std::unique_ptr<hesim::statmods::obs_index>(new hesim::statmods::obs_index(id_object)); | |
} | |
int n_samples = transmod->get_n_samples(); | |
int n_strategies = transmod->get_n_strategies(); | |
int n_patients = transmod->get_n_patients(); // actually, the number of cohorts | |
int n_states = transmod->trans_mat_.n_states_; | |
int n_times = times.size(); | |
int N = n_samples * | |
n_strategies * | |
n_patients * | |
n_states* | |
n_times; | |
hesim::stateprobs_out out(N); | |
int N2 = n_samples * | |
n_strategies * | |
n_patients * | |
n_states * | |
(1+(valid_R_QALYsStateVal ? 1 : 0)+n_costs); // LYs, QALYs, cost categories | |
hesim::ev_out out2(N2); | |
int counter = 0, counter2 = 0; | |
// Loop | |
for (int s = 0; s < n_samples; ++s){ | |
if (progress > 0){ | |
if ((s + 1) % progress == 0){ // R-based indexing | |
Rcpp::Rcout << "sample = " << s + 1 << std::endl; | |
} | |
} | |
for (int k = 0; k < n_strategies; ++k){ | |
transmod->obs_index_.set_strategy_index(k); | |
for (int i = 0; i < n_patients; ++i){ | |
transmod->obs_index_.set_patient_index(i); | |
arma::vec p0 = arma::zeros(n_states*(3+R_CostsStateVals.size())); | |
p0[start_state[i]] = 1.0; | |
int n_trans = transmod->trans_mat_.n_trans_; | |
arma::uvec from(n_trans); | |
arma::uvec to(n_trans); | |
for (int state_ = 0; state_<n_states; ++state_) { | |
std::vector<int> trans_ids = transmod->trans_mat_.trans_id(state_); | |
std::vector<int> tos = transmod->trans_mat_.to(state_); | |
int n_trans_state = trans_ids.size(); | |
for (int trans_ = 0; trans_ < n_trans_state; ++trans_){ // NB: within a state | |
int trans_id = trans_ids[trans_]; | |
from(trans_id) = state_; | |
to(trans_id) = tos[trans_]; | |
} | |
} | |
arma::mat report(n_times,p0.size()); | |
report.row(0) = p0.t(); | |
auto stepper = make_dense_output(1.0e-10, 1.0e-10, runge_kutta_dopri5<state_type>()); | |
for (size_t j=1; j<n_times; j++) { | |
size_t n = integrate_adaptive(stepper, | |
[&](const state_type &Y , state_type &dYdt, const double t) | |
{ | |
arma::vec rates(n_trans); | |
arma::vec utilities(n_states); | |
arma::mat costs_by_state(n_states,n_costs); | |
arma::mat costs_by_transition(n_trans,n_costs); | |
double age = start_age[i]+t; | |
double tstar = std::max(eps,t); | |
// rate calculations | |
for (int state_ = 0; state_<n_states; ++state_) { | |
for (int trans_ = 0; trans_ < n_trans; ++trans_){ | |
if (clock == "forward"){ | |
rates[trans_] = transmod->summary(trans_, s, {tstar}, "hazard")[0]; | |
} else { // clock == "mixt" | |
if (transition_types[trans_] == tt_age){ | |
rates[trans_] = transmod->summary(trans_, s, {age}, "hazard")[0]; | |
} else { // transition_types[trans_] == tt_time (tt_reset is *not* currently available) | |
rates[trans_] = transmod->summary(trans_, s, {tstar}, "hazard")[0]; | |
} | |
} | |
} // end loop over transitions | |
} // end loop over states | |
// utility input calculations | |
if (valid_R_QALYsStateVal) { | |
int t_index_qalys = hesim::hesim_bound(t, obs_index_qalys->time_start_); | |
for (int state_ = 0; state_<n_states; ++state_) { | |
int obs_qalys = (*obs_index_qalys)(k, // strategy | |
i, // patient | |
state_, | |
t_index_qalys); | |
utilities(state_) = qalys_lookup->sim(s, obs_qalys, type); | |
} // end loop over states | |
} | |
// cost input calculations | |
for (int cost_=0; cost_<n_costs; ++cost_) { | |
int t_index_costs = hesim::hesim_bound(t, obs_index_costs[cost_].time_start_); | |
if (costs_lookup[cost_].method_ == "wlos") { | |
for (int state_ = 0; state_<n_states; ++state_) { | |
int obs_costs = obs_index_costs[cost_](k, // strategy | |
i, // patient | |
state_, | |
t_index_costs); | |
costs_by_state(state_, cost_) += costs_lookup[cost_].sim(s, obs_costs, type); | |
} // end loop over states | |
} else if (costs_lookup[cost_].method_ == "starting") { | |
for (int trans_=0; trans_<n_trans; ++trans_) { | |
int obs_costs = obs_index_costs[cost_](k, // strategy | |
i, // patient | |
to(trans_), | |
t_index_costs); | |
costs_by_state(to(trans_), cost_) = costs_lookup[cost_].sim(s, obs_costs, type); | |
} // end loop over transitions | |
} else { // costs_lookup[cost_].method_ == "transition" | |
for (int trans_=0; trans_<n_trans; ++trans_) { | |
int obs_costs = obs_index_costs[cost_](k, // strategy | |
i, // patient | |
trans_, // assumes transition | |
t_index_costs); | |
costs_by_transition(trans_, cost_) = costs_lookup[cost_].sim(s, obs_costs, type); // by trans_ | |
} // end loop over transitions | |
} // end case: "transition" | |
} // end loop over costs | |
dYdt = dYdt*0.0; | |
arma::vec delta = Y(from) % rates; | |
dYdt(to) += delta; | |
dYdt(from) -= delta; | |
double drr_utilities = std::exp(-dr_qalys*t); | |
double drr_costs = std::exp(-dr_costs*t); | |
// undiscounted life-years | |
dYdt(arma::span(n_states,2*n_states-1)) = Y(arma::span(0,n_states-1)); | |
// discounted utilities | |
dYdt(arma::span(2*n_states,3*n_states-1)) = utilities % Y(arma::span(0,n_states-1))*drr_utilities; | |
// discounted costs | |
for (int cost_=0; cost_<n_costs; ++cost_) { | |
if (costs_lookup[cost_].method_ == "wlos") | |
dYdt(arma::span((3+cost_)*n_states,(4+cost_)*n_states-1)) += costs_by_state.col(cost_) % Y(arma::span(0,n_states-1))*drr_costs; | |
else if (costs_lookup[cost_].method_ == "starting") { | |
for (int trans_=0; trans_<n_trans; ++trans_) | |
dYdt(to[trans_] + (3+cost_)*n_states) += costs_by_state(to[trans_],cost_) * Y(from[trans_]) * rates(trans_) *drr_costs; | |
} else { // costs_lookup[cost_].method_ == "transition" | |
for (int trans_=0; trans_<n_trans; ++trans_) | |
dYdt(from[trans_] + (3+cost_)*n_states) += // costs arbitrarily assigned to the state of origin (from state) | |
costs_by_transition(trans_,cost_) * Y(from(trans_)) * rates(trans_) * drr_costs; | |
} | |
} | |
}, | |
p0, | |
times[j-1], | |
times[j], | |
times[j]-times[j-1]); | |
report.row(j) = p0.t(); | |
} // end j times_ loop | |
// Output | |
for (int h = 0; h < n_states; ++h){ | |
for (int ti = 0; ti < n_times; ++ti){ | |
CTSTM_OUT(out,counter); | |
out.t_[counter] = times[ti]; | |
out.prob_[counter] = report(ti, h); | |
++counter; | |
} // end cycles loop | |
// report for life-years | |
CTSTM_OUT(out2,counter2); | |
out2.dr_[counter2] = 0.0; // assume no discounting for life-years | |
out2.outcome_[counter2] = "ly"; | |
out2.value_[counter2] = report(n_times-1,n_states+h); | |
++counter2; | |
// report for qalys | |
if (valid_R_QALYsStateVal) { | |
CTSTM_OUT(out2,counter2); | |
out2.dr_[counter2] = dr_qalys; | |
out2.outcome_[counter2] = "qaly"; | |
out2.value_[counter2] = report(n_times-1,2*n_states+h); | |
++counter2; | |
} | |
// report for costs | |
for (int cost_=0; cost_<n_costs; ++cost_) { | |
CTSTM_OUT(out2,counter2); | |
out2.dr_[counter2] = dr_costs; | |
out2.outcome_[counter2] = "Category " + std::to_string(cost_+1); | |
out2.value_[counter2] = report(n_times-1,(3+cost_)*n_states+h); | |
++counter2; | |
} // end cost category loop | |
} // end state loop | |
} // end patient loop | |
} // end strategy loop | |
} // end parameter sampling loop | |
// Return | |
using namespace Rcpp; | |
return(List::create(_["stateprobs"]=out.create_R_data_frame(), | |
_["ev"]=out2.create_R_data_frame())); | |
} | |
#undef CTSTM_OUT | |
// test example that does not use hesim | |
class TestODE { | |
public: | |
typedef arma::vec state_type; | |
size_t n_states; | |
double discount_rate; | |
TestODE(double discount_rate = 0.0) | |
: n_states(4), discount_rate(discount_rate) { } | |
inline double hweibull(const double t, const double shape, const double scale) { | |
return R::dweibull(std::max(1e-100,t),shape,scale,0)/R::pweibull(t,shape,scale,0,0); | |
} | |
void operator() (const state_type &Y , state_type &dYdt, const double t) | |
{ | |
run_step(Y, dYdt, t); | |
} | |
void run_step(const state_type &Y , state_type &dYdt, const double t) | |
{ | |
using namespace arma; | |
// conv_to<uvec>::from(an_ivec) | |
// vec rates = {0.1, 0.2, 0.3}; | |
vec rates = {hweibull(t,0.8,1.0), hweibull(t,0.8,1.0), | |
hweibull(t,0.8,2.0), hweibull(t,0.8,3.0)}; | |
vec utilities = {0.9, 0.8, 0.7, 0.0}; | |
vec costs_per_unit_time = {10000.0, 20000.0, 30000.0, 0.0}; // by state | |
vec costs_per_transition = {10.0, 15.0, 20.0, 30.0}; // by transition | |
vec starting_costs = {0.0, 2000.0, 3000.0, 0.0}; // by state | |
uvec from = {0, 1, 1, 2}; | |
uvec to = {1, 0, 2, 3}; | |
dYdt = dYdt*0.0; | |
// state occupation/transition probabilities | |
vec delta = Y(from) % rates; | |
dYdt(to) += delta; | |
dYdt(from) -= delta; | |
// double dr = 1.0/pow(1.0+discount_rate,t); | |
double dr = std::exp(-discount_rate*t); | |
// undiscounted life-years | |
dYdt(span(n_states,2*n_states-1)) = Y(span(0,n_states-1)); | |
// discounted utilities | |
dYdt(span(2*n_states,3*n_states-1)) = utilities % Y(span(0,n_states-1))*dr; | |
// discounted costs | |
dYdt(span(3*n_states,4*n_states-1)) = costs_per_unit_time % Y(span(0,n_states-1))*dr; | |
// discounted transition costs | |
for (int j=0; j<4; ++j) | |
dYdt(from[j] + 4*n_states) += costs_per_transition[j] * Y(from[j]) * rates(j) * dr; | |
// discounted starting costs | |
for (int j=0; j<4; ++j) | |
dYdt(to[j] + 5*n_states) += starting_costs[to[j]] * Y(from[j]) * rates(j) * dr; | |
} | |
arma::mat run(arma::vec p0, arma::vec times) { | |
using namespace boost::numeric::odeint; | |
size_t n_times = times.size(); | |
// combine the results | |
arma::mat combined(n_times,p0.size()); | |
combined.row(0) = p0.t(); | |
auto stepper = make_dense_output(1.0e-10, 1.0e-10, runge_kutta_dopri5<state_type>()); | |
for (size_t i=1; i<n_times; i++) { | |
size_t n = integrate_adaptive(stepper, | |
[this](const state_type &Y , state_type &dYdt, const double t) { | |
this->run_step(Y,dYdt,t); | |
}, | |
p0, | |
times[i-1], | |
times[i], | |
times[i]-times[i-1]); | |
combined.row(i) = p0.t(); | |
} | |
return combined; | |
} | |
}; | |
// [[Rcpp::export]] | |
Rcpp::List runTestODE(arma::vec p0, arma::vec times, double discount_rate = 0.0) { | |
using namespace Rcpp; | |
return List::create(_("times")=times, | |
_("Y")=TestODE(discount_rate).run(p0,times)); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
library("RcppArmadillo") | |
library("Rcpp") | |
library("hesim") | |
library("flexsurv") | |
sourceCpp("code/** | |
Requires the transition_costs branch | |
Mark Clements | |
2024-07-08 | |
*/ | |
#include <boost/numeric/odeint.hpp> | |
#include <RcppArmadillo.h> | |
#include <hesim.h> | |
#include <hesim/ctstm/ctstm.h> | |
#include <hesim/statevals.h> | |
#include <hesim/dtstm.h> | |
// [[Rcpp::depends(RcppArmadillo)]] | |
// [[Rcpp::depends(hesim)]] | |
// [[Rcpp::depends(BH)]] | |
// Boilerplate code to ensure that arma plays nicely with boost::numeric::odeint | |
namespace boost { | |
namespace numeric { | |
namespace odeint { | |
template <> | |
struct is_resizeable<arma::vec> | |
{ | |
typedef boost::true_type type; | |
const static bool value = type::value; | |
}; | |
template <> | |
struct same_size_impl<arma::vec, arma::vec> | |
{ | |
static bool same_size(const arma::vec& x, const arma::vec& y) | |
{ | |
return x.size() == y.size(); | |
} | |
}; | |
template<> | |
struct resize_impl<arma::vec, arma::vec> | |
{ | |
static void resize(arma::vec& v1, const arma::vec& v2) | |
{ | |
v1.resize(v2.size()); | |
} | |
}; | |
} | |
} | |
} // namespace boost::numeric::odeint | |
#define CTSTM_OUT(OBJECT,COUNTER) \ | |
OBJECT.sample_[COUNTER] = s; \ | |
OBJECT.strategy_id_[COUNTER] = k; \ | |
OBJECT.patient_id_[COUNTER] = i; \ | |
OBJECT.grp_id_[COUNTER] = transmod->obs_index_.get_grp_id(); \ | |
OBJECT.patient_wt_[COUNTER] = transmod->obs_index_.get_patient_wt(); \ | |
OBJECT.state_id_[COUNTER] = h; | |
/***************************************************************************//** | |
* @ingroup ctstm | |
* Simulate disease progression (i.e., a path through a multi-state model). | |
* This function is exported to @c R and used in @c CohortCtstmTrans$sim_stateprobs() and | |
* in @c CohortCtstm$sim_disase(). | |
* @param R_CtstmTrans An R object of class @c CohortCtstmTrans. | |
* @param start_state The starting health state for each patient and random sample | |
* of the parameter sets. (TODO: allow for probabilities in the initial states.) | |
* @param start_age The starting age of each patient in the simulation. | |
* @param start_time The starting time of the simulation. | |
* @param max_t The maximum time to simulate the model for. | |
* @param max_age The maximum age that a patient can live to. | |
* @return An R data frame of the same format as stateprobs_out. | |
******************************************************************************/ | |
// [[Rcpp::export]] | |
Rcpp::List C_cohort_ctstm_sim(Rcpp::Environment R_CtstmTrans, | |
Rcpp::List R_CostsStateVals, | |
Rcpp::Environment R_QALYsStateVal, | |
std::vector<int> start_state, // by patient | |
std::vector<double> start_age, // by patient | |
std::vector<double> times, // common | |
std::string clock, | |
std::vector<int> transition_types, | |
int progress = 0, | |
double dr_qalys = 0.0, | |
double dr_costs = 0.0, | |
std::string type="predict", | |
double eps = 1e-100) { | |
using namespace boost::numeric::odeint; | |
typedef arma::vec state_type; | |
enum TransitionType {tt_time, tt_age}; | |
// Initialize | |
std::unique_ptr<hesim::ctstm::transmod> transmod = hesim::ctstm::transmod::create(R_CtstmTrans); | |
int n_costs = R_CostsStateVals.size(); | |
std::vector<hesim::statevals> costs_lookup; | |
std::vector<hesim::statmods::obs_index> obs_index_costs; | |
for (int cost_=0; cost_<n_costs; ++cost_) { | |
costs_lookup.push_back(hesim::statevals(Rcpp::as<Rcpp::Environment>(R_CostsStateVals[cost_]))); | |
obs_index_costs.push_back(hesim::statmods::obs_index(hesim::statmods::get_id_object(Rcpp::as<Rcpp::Environment>(R_CostsStateVals[cost_])))); | |
} | |
// use unique_ptr because statevals and obs_index can be null and do not have null constructors | |
std::unique_ptr<hesim::statevals> qalys_lookup; | |
std::unique_ptr<hesim::statmods::obs_index> obs_index_qalys; | |
bool valid_R_QALYsStateVal = R_QALYsStateVal.exists("method"); | |
if (valid_R_QALYsStateVal) { | |
qalys_lookup = std::unique_ptr<hesim::statevals>(new hesim::statevals(R_QALYsStateVal)); | |
auto id_object = hesim::statmods::get_id_object(R_QALYsStateVal); | |
obs_index_qalys = | |
std::unique_ptr<hesim::statmods::obs_index>(new hesim::statmods::obs_index(id_object)); | |
} | |
int n_samples = transmod->get_n_samples(); | |
int n_strategies = transmod->get_n_strategies(); | |
int n_patients = transmod->get_n_patients(); // actually, the number of cohorts | |
int n_states = transmod->trans_mat_.n_states_; | |
int n_times = times.size(); | |
int N = n_samples * | |
n_strategies * | |
n_patients * | |
n_states* | |
n_times; | |
hesim::stateprobs_out out(N); | |
int N2 = n_samples * | |
n_strategies * | |
n_patients * | |
n_states * | |
(1+(valid_R_QALYsStateVal ? 1 : 0)+n_costs); // LYs, QALYs, cost categories | |
hesim::ev_out out2(N2); | |
int counter = 0, counter2 = 0; | |
// Loop | |
for (int s = 0; s < n_samples; ++s){ | |
if (progress > 0){ | |
if ((s + 1) % progress == 0){ // R-based indexing | |
Rcpp::Rcout << "sample = " << s + 1 << std::endl; | |
} | |
} | |
for (int k = 0; k < n_strategies; ++k){ | |
transmod->obs_index_.set_strategy_index(k); | |
for (int i = 0; i < n_patients; ++i){ | |
transmod->obs_index_.set_patient_index(i); | |
arma::vec p0 = arma::zeros(n_states*(3+R_CostsStateVals.size())); | |
p0[start_state[i]] = 1.0; | |
int n_trans = transmod->trans_mat_.n_trans_; | |
arma::uvec from(n_trans); | |
arma::uvec to(n_trans); | |
for (int state_ = 0; state_<n_states; ++state_) { | |
std::vector<int> trans_ids = transmod->trans_mat_.trans_id(state_); | |
std::vector<int> tos = transmod->trans_mat_.to(state_); | |
int n_trans_state = trans_ids.size(); | |
for (int trans_ = 0; trans_ < n_trans_state; ++trans_){ // NB: within a state | |
int trans_id = trans_ids[trans_]; | |
from(trans_id) = state_; | |
to(trans_id) = tos[trans_]; | |
} | |
} | |
arma::mat report(n_times,p0.size()); | |
report.row(0) = p0.t(); | |
auto stepper = make_dense_output(1.0e-10, 1.0e-10, runge_kutta_dopri5<state_type>()); | |
for (size_t j=1; j<n_times; j++) { | |
size_t n = integrate_adaptive(stepper, | |
[&](const state_type &Y , state_type &dYdt, const double t) | |
{ | |
arma::vec rates(n_trans); | |
arma::vec utilities(n_states); | |
arma::mat costs_by_state(n_states,n_costs); | |
arma::mat costs_by_transition(n_trans,n_costs); | |
double age = start_age[i]+t; | |
double tstar = std::max(eps,t); | |
// rate calculations | |
for (int state_ = 0; state_<n_states; ++state_) { | |
for (int trans_ = 0; trans_ < n_trans; ++trans_){ | |
if (clock == "forward"){ | |
rates[trans_] = transmod->summary(trans_, s, {tstar}, "hazard")[0]; | |
} else { // clock == "mixt" | |
if (transition_types[trans_] == tt_age){ | |
rates[trans_] = transmod->summary(trans_, s, {age}, "hazard")[0]; | |
} else { // transition_types[trans_] == tt_time (tt_reset is *not* currently available) | |
rates[trans_] = transmod->summary(trans_, s, {tstar}, "hazard")[0]; | |
} | |
} | |
} // end loop over transitions | |
} // end loop over states | |
// utility input calculations | |
if (valid_R_QALYsStateVal) { | |
int t_index_qalys = hesim::hesim_bound(t, obs_index_qalys->time_start_); | |
for (int state_ = 0; state_<n_states; ++state_) { | |
int obs_qalys = (*obs_index_qalys)(k, // strategy | |
i, // patient | |
state_, | |
t_index_qalys); | |
utilities(state_) = qalys_lookup->sim(s, obs_qalys, type); | |
} // end loop over states | |
} | |
// cost input calculations | |
for (int cost_=0; cost_<n_costs; ++cost_) { | |
int t_index_costs = hesim::hesim_bound(t, obs_index_costs[cost_].time_start_); | |
if (costs_lookup[cost_].method_ == "wlos") { | |
for (int state_ = 0; state_<n_states; ++state_) { | |
int obs_costs = obs_index_costs[cost_](k, // strategy | |
i, // patient | |
state_, | |
t_index_costs); | |
costs_by_state(state_, cost_) += costs_lookup[cost_].sim(s, obs_costs, type); | |
} // end loop over states | |
} else if (costs_lookup[cost_].method_ == "starting") { | |
for (int trans_=0; trans_<n_trans; ++trans_) { | |
int obs_costs = obs_index_costs[cost_](k, // strategy | |
i, // patient | |
to(trans_), | |
t_index_costs); | |
costs_by_state(to(trans_), cost_) = costs_lookup[cost_].sim(s, obs_costs, type); | |
} // end loop over transitions | |
} else { // costs_lookup[cost_].method_ == "transition" | |
for (int trans_=0; trans_<n_trans; ++trans_) { | |
int obs_costs = obs_index_costs[cost_](k, // strategy | |
i, // patient | |
trans_, // assumes transition | |
t_index_costs); | |
costs_by_transition(trans_, cost_) = costs_lookup[cost_].sim(s, obs_costs, type); // by trans_ | |
} // end loop over transitions | |
} // end case: "transition" | |
} // end loop over costs | |
dYdt = dYdt*0.0; | |
arma::vec delta = Y(from) % rates; | |
dYdt(to) += delta; | |
dYdt(from) -= delta; | |
double drr_utilities = std::exp(-dr_qalys*t); | |
double drr_costs = std::exp(-dr_costs*t); | |
// undiscounted life-years | |
dYdt(arma::span(n_states,2*n_states-1)) = Y(arma::span(0,n_states-1)); | |
// discounted utilities | |
dYdt(arma::span(2*n_states,3*n_states-1)) = utilities % Y(arma::span(0,n_states-1))*drr_utilities; | |
// discounted costs | |
for (int cost_=0; cost_<n_costs; ++cost_) { | |
if (costs_lookup[cost_].method_ == "wlos") | |
dYdt(arma::span((3+cost_)*n_states,(4+cost_)*n_states-1)) += costs_by_state.col(cost_) % Y(arma::span(0,n_states-1))*drr_costs; | |
else if (costs_lookup[cost_].method_ == "starting") { | |
for (int trans_=0; trans_<n_trans; ++trans_) | |
dYdt(to[trans_] + (3+cost_)*n_states) += costs_by_state(to[trans_],cost_) * Y(from[trans_]) * rates(trans_) *drr_costs; | |
} else { // costs_lookup[cost_].method_ == "transition" | |
for (int trans_=0; trans_<n_trans; ++trans_) | |
dYdt(from[trans_] + (3+cost_)*n_states) += // costs arbitrarily assigned to the state of origin (from state) | |
costs_by_transition(trans_,cost_) * Y(from(trans_)) * rates(trans_) * drr_costs; | |
} | |
} | |
}, | |
p0, | |
times[j-1], | |
times[j], | |
times[j]-times[j-1]); | |
report.row(j) = p0.t(); | |
} // end j times_ loop | |
// Output | |
for (int h = 0; h < n_states; ++h){ | |
for (int ti = 0; ti < n_times; ++ti){ | |
CTSTM_OUT(out,counter); | |
out.t_[counter] = times[ti]; | |
out.prob_[counter] = report(ti, h); | |
++counter; | |
} // end cycles loop | |
// report for life-years | |
CTSTM_OUT(out2,counter2); | |
out2.dr_[counter2] = 0.0; // assume no discounting for life-years | |
out2.outcome_[counter2] = "ly"; | |
out2.value_[counter2] = report(n_times-1,n_states+h); | |
++counter2; | |
// report for qalys | |
if (valid_R_QALYsStateVal) { | |
CTSTM_OUT(out2,counter2); | |
out2.dr_[counter2] = dr_qalys; | |
out2.outcome_[counter2] = "qaly"; | |
out2.value_[counter2] = report(n_times-1,2*n_states+h); | |
++counter2; | |
} | |
// report for costs | |
for (int cost_=0; cost_<n_costs; ++cost_) { | |
CTSTM_OUT(out2,counter2); | |
out2.dr_[counter2] = dr_costs; | |
out2.outcome_[counter2] = "Category " + std::to_string(cost_+1); | |
out2.value_[counter2] = report(n_times-1,(3+cost_)*n_states+h); | |
++counter2; | |
} // end cost category loop | |
} // end state loop | |
} // end patient loop | |
} // end strategy loop | |
} // end parameter sampling loop | |
// Return | |
using namespace Rcpp; | |
return(List::create(_["stateprobs"]=out.create_R_data_frame(), | |
_["ev"]=out2.create_R_data_frame())); | |
} | |
#undef CTSTM_OUT | |
// test example that does not use hesim | |
class TestODE { | |
public: | |
typedef arma::vec state_type; | |
size_t n_states; | |
double discount_rate; | |
TestODE(double discount_rate = 0.0) | |
: n_states(4), discount_rate(discount_rate) { } | |
inline double hweibull(const double t, const double shape, const double scale) { | |
return R::dweibull(std::max(1e-100,t),shape,scale,0)/R::pweibull(t,shape,scale,0,0); | |
} | |
void operator() (const state_type &Y , state_type &dYdt, const double t) | |
{ | |
run_step(Y, dYdt, t); | |
} | |
void run_step(const state_type &Y , state_type &dYdt, const double t) | |
{ | |
using namespace arma; | |
// conv_to<uvec>::from(an_ivec) | |
// vec rates = {0.1, 0.2, 0.3}; | |
vec rates = {hweibull(t,0.8,1.0), hweibull(t,0.8,1.0), | |
hweibull(t,0.8,2.0), hweibull(t,0.8,3.0)}; | |
vec utilities = {0.9, 0.8, 0.7, 0.0}; | |
vec costs_per_unit_time = {10000.0, 20000.0, 30000.0, 0.0}; // by state | |
vec costs_per_transition = {10.0, 15.0, 20.0, 30.0}; // by transition | |
vec starting_costs = {0.0, 2000.0, 3000.0, 0.0}; // by state | |
uvec from = {0, 1, 1, 2}; | |
uvec to = {1, 0, 2, 3}; | |
dYdt = dYdt*0.0; | |
// state occupation/transition probabilities | |
vec delta = Y(from) % rates; | |
dYdt(to) += delta; | |
dYdt(from) -= delta; | |
// double dr = 1.0/pow(1.0+discount_rate,t); | |
double dr = std::exp(-discount_rate*t); | |
// undiscounted life-years | |
dYdt(span(n_states,2*n_states-1)) = Y(span(0,n_states-1)); | |
// discounted utilities | |
dYdt(span(2*n_states,3*n_states-1)) = utilities % Y(span(0,n_states-1))*dr; | |
// discounted costs | |
dYdt(span(3*n_states,4*n_states-1)) = costs_per_unit_time % Y(span(0,n_states-1))*dr; | |
// discounted transition costs | |
for (int j=0; j<4; ++j) | |
dYdt(from[j] + 4*n_states) += costs_per_transition[j] * Y(from[j]) * rates(j) * dr; | |
// discounted starting costs | |
for (int j=0; j<4; ++j) | |
dYdt(to[j] + 5*n_states) += starting_costs[to[j]] * Y(from[j]) * rates(j) * dr; | |
} | |
arma::mat run(arma::vec p0, arma::vec times) { | |
using namespace boost::numeric::odeint; | |
size_t n_times = times.size(); | |
// combine the results | |
arma::mat combined(n_times,p0.size()); | |
combined.row(0) = p0.t(); | |
auto stepper = make_dense_output(1.0e-10, 1.0e-10, runge_kutta_dopri5<state_type>()); | |
for (size_t i=1; i<n_times; i++) { | |
size_t n = integrate_adaptive(stepper, | |
[this](const state_type &Y , state_type &dYdt, const double t) { | |
this->run_step(Y,dYdt,t); | |
}, | |
p0, | |
times[i-1], | |
times[i], | |
times[i]-times[i-1]); | |
combined.row(i) = p0.t(); | |
} | |
return combined; | |
} | |
}; | |
// [[Rcpp::export]] | |
Rcpp::List runTestODE(arma::vec p0, arma::vec times, double discount_rate = 0.0) { | |
using namespace Rcpp; | |
return List::create(_("times")=times, | |
_("Y")=TestODE(discount_rate).run(p0,times)); | |
} | |
.cpp") | |
## Test code | |
## Simulation data | |
strategies <- data.frame(strategy_id = 1L) | |
patients <- data.frame(patient_id = 1:2, intercept=1) | |
## Multi-state model with transition specific models (as per testODE in the C++ code) | |
tmat <- rbind(c(NA, 1, NA, NA), | |
c(2, NA, 3, NA), | |
c(NA, NA, NA, 4), | |
c(NA, NA, NA, NA)) | |
## short utility function | |
make_param <- function(shape,scale,dist="weibull") | |
params_surv( | |
coefs = list( | |
shape = data.frame( | |
intercept = log(shape) | |
), | |
scale = data.frame( | |
intercept = log(scale) | |
) | |
), | |
dist = dist) | |
fits = params_surv_list(make_param(0.8, 1.0), make_param(0.8, 1.0), | |
make_param(0.8, 2.0), make_param(0.8, 3.0)) | |
## Simulation model | |
hesim_dat <- hesim_data(strategies = strategies, | |
patients = patients) | |
fits_data <- expand(hesim_dat) | |
## create_IndivCtstmTrans can also be used for cCTSTM:) | |
transmod <- create_IndivCtstmTrans(fits, input_data = fits_data, | |
trans_mat = tmat, | |
start_age=50, | |
start_state=1, | |
clock="mixt", | |
transition_types=c("time","time","time","time")) | |
## utilities | |
utilities_tbl = stateval_tbl(data.frame(state_id=1:4, | |
est=c(0.9, 0.8, 0.7, 0.0)), | |
dist="fixed") | |
utilities = create_StateVals(utilities_tbl, hesim_data=hesim_dat, n=1) | |
## costs | |
costs_wlos_tbl = stateval_tbl(data.frame(state_id=1:4, | |
est=c(10000.0, 20000.0, 30000.0, 0.0)), | |
dist="fixed") | |
costs_wlos = create_StateVals(costs_wlos_tbl, hesim_data=hesim_dat, n=1) | |
## for the transition costs, "state_id" is actually the id for the transition | |
costs_transition_tbl <- stateval_tbl(tbl = data.frame(state_id = 1:4, | |
est = c(10, 15, 20, 30)), | |
dist = "fixed") | |
costs_transition <- create_StateVals(costs_transition_tbl, n = 1, | |
time_reset = FALSE, method = "transition", | |
hesim_data = hesim_dat) | |
costs_starting_tbl = stateval_tbl(data.frame(state_id=1:4, | |
est=c(0.0, 2000.0, 3000.0, 0.0)), | |
dist="fixed") | |
costs_starting = create_StateVals(costs_starting_tbl, hesim_data=hesim_dat, n=1, | |
method="starting") | |
test = C_cohort_ctstm_sim(transmod, | |
list(costs_wlos,costs_transition,costs_starting), | |
utilities, | |
start_state=transmod$start_state - 1L, | |
start_age=transmod$start_age, | |
times=c(0,1,2,10), | |
clock=transmod$clock, | |
transition_types=match(transmod$transition_types, c("time","age")) - 1L, | |
progress=0, | |
dr_qalys=0.03, | |
dr_costs=0.03) | |
local({ | |
names = c("ly","qaly","Category 1","Category 2","Category 3") | |
test2 = runTestODE(c(1,0,0,0, rep(0,4*5)), c(0,1,2,10), 0.03) | |
Y = test2$Y |> "dim<-"(c(4,4,6)) | |
(Y[4,,-1] - xtabs(value~state_id+outcome,test$ev,patient_id==1)[,names]) |> abs() |> max() | |
}) | |
## runTestODE(c(1,0,0,0, rep(0,4*5)), c(0,1,2,10), 0.03) | |
# CohortCtstm class ------------------------------------------------------------- | |
#' Individual-level continuous time state transition model | |
#' | |
#' @description | |
#' Simulate outcomes from a cohort-level continuous time state transition | |
#' model (CTSTM). The class supports "clock-forward" (i.e., Markov), and mixtures of | |
#' clock-age and clock-forward multi-state models as described in | |
#' [`IndivCtstmTrans`]. | |
#' @format An [R6::R6Class] object. | |
#' @seealso The [`IndivCtstmTrans`] documentation | |
#' describes the class for the transition model and the [`StateVals`] documentation | |
#' describes the class for the cost and utility models. An [`IndivCtstmTrans`] | |
#' object is typically created using [create_IndivCtstmTrans()]. | |
#' | |
#' There are currently no relevant vignettes. | |
#' @name CohortCtstm | |
NULL | |
#' @param dr Discount rate. | |
#' @param type `"predict"` for mean values or `"random"` for random samples | |
#' as in `$sim()` in [`StateVals`]. | |
#' @export | |
CohortCtstm <- R6::R6Class("CohortCtstm", | |
public = list( | |
#' @field trans_model The model for health state transitions. Must be an object | |
#' of class [`IndivCtstmTrans`]. | |
trans_model = NULL, | |
#' @field utility_model The model for health state utility. Must be an object of | |
#' class [`StateVals`]. | |
utility_model = NULL, | |
#' @field cost_models The models used to predict costs by health state. | |
#' Must be a list of objects of class [`StateVals`], where each element of the | |
#' list represents a different cost category. | |
cost_models = NULL, | |
#' @field stateprobs_ An object of class [`stateprobs`] simulated using `$sim_stateprobs()`. | |
stateprobs_ = NULL, | |
#' @field qalys_ An object of class [`qalys`] simulated using `$sim_qalys()`. | |
qalys_ = NULL, | |
#' @field costs_ An object of class [`costs`] simulated using `$sim_costs()`. | |
costs_ = NULL, | |
#' @field times_ A vector of times. | |
times_ = NULL, | |
#' @description | |
#' Create a new `CohortCtstm` object. | |
#' @param trans_model The `trans_model` field. | |
#' @param utility_model The `utility_model` field. | |
#' @param cost_models The `cost_models` field. | |
#' @return A new `CohortCtstm` object. | |
initialize = function(trans_model = NULL, utility_model = NULL, cost_models = NULL) { | |
self$trans_model <- trans_model | |
self$utility_model <- if (is.null(utility_model)) new.env() else utility_model | |
self$cost_models <- cost_models | |
}, | |
#' @description | |
#' Simulate quality-adjusted life-years (QALYs) as a function of | |
#' `utility_model` and costs as a function of `cost_models`. | |
#' @param lys If `TRUE`, then life-years are simulated in addition to QALYs. | |
#' @return An instance of `self` with simulated output of | |
#' class [qalys] stored in `qalys_` and class [costs] stored in `costs_`.. | |
sim = function(t=c(0,1), dr_qalys = .03, dr_costs=.03, type = c("predict", "random"), | |
lys = TRUE, progress=NULL, | |
by_patient = TRUE, | |
eps=1e-100){ | |
if(!inherits(self$utility_model, "StateVals")){ | |
stop("'utility_model' must be an object of class 'StateVals'", | |
call. = FALSE) | |
} | |
if(!is.list(self$cost_models)){ | |
stop("'cost_models' must be a list of objects of class 'StateVals'", | |
call. = FALSE) | |
} else{ | |
for (i in 1:length(self$cost_models)){ | |
if (!inherits(self$cost_models[[i]], "StateVals")){ | |
stop("'cost_models' must be a list of objects of class 'StateVals'", | |
call. = FALSE) | |
} | |
} | |
} | |
type <- match.arg(type) | |
hesim:::check_dr(dr_qalys) | |
hesim:::check_dr(dr_costs) | |
self$times_ <- t | |
## browser() | |
C_ev <- C_cohort_ctstm_sim(R_CtstmTrans=self$trans_model, | |
R_CostsStateVals=self$cost_models, | |
R_QALYsStateVal=self$utility_model, | |
start_state=self$trans_model$start_state - 1L, | |
start_age=self$trans_model$start_age, | |
times=self$times_, | |
clock=self$trans_model$clock, | |
transition_types=match(self$trans_model$transition_types, | |
c("time","age")) - 1L, | |
progress=if(is.null(progress)) 0 else progress, | |
dr_qalys=dr_qalys, | |
dr_costs=dr_costs, | |
type=match.arg(type), | |
eps=eps) | |
self$stateprobs_ <- as.data.table(C_ev$stateprobs) | |
ev <- as.data.table(C_ev$ev) | |
self$qalys_ <- ev[outcome=="qaly"][ | |
,`:=`(category='qalys',qalys=value,value=NULL,outcome=NULL)] | |
if (lys) | |
self$qalys_$ly = ev[outcome=="ly","value"] | |
self$costs_ <- do.call(rbind, | |
lapply(unique(grep("Category",ev$outcome,value=TRUE)), | |
function(pattern) { | |
ev2 <- ev[outcome==pattern][ | |
,`:=`(category=pattern,costs=value,value=NULL,outcome=NULL)] | |
ev2 | |
})) | |
invisible(self) | |
}, | |
#' @description | |
#' Summarize costs and QALYs so that cost-effectiveness analysis can be performed. | |
#' See [summarize_ce()]. | |
#' @param by_grp If `TRUE`, then costs and QALYs are computed by subgroup. If | |
#' `FALSE`, then costs and QALYs are aggregated across all patients (and subgroups). | |
summarize = function(by_grp = FALSE) { | |
hesim:::check_summarize(self) | |
hesim:::summarize_ce(self$costs_, self$qalys_, by_grp) | |
} | |
) # end public | |
) # end class | |
CohortCtstm$new(trans_model=transmod, | |
cost_models=list(costs_wlos,costs_transition,costs_starting), | |
utility_model=utilities)$sim(t=c(0,1,2,10))$summarize() | |
1 | |
## ## Reconstruct hesim_data from input_mats | |
## ## This is essentially the reverse of hesim:::expand.hesim_data | |
## unexpand = function(self) { | |
## stopifnot(inherits(self, "input_mats")) | |
## out = list() | |
## if ("strategy_id" %in% names(self)) | |
## out$strategies = data.frame(strategy_id = unique(self$strategy_id)) | |
## if ("patient_id" %in% names(self)) { | |
## patient_data = as.data.frame(as.data.table(self)) | |
## patient_data[c("strategy_id","group_id","time_start")] = NULL | |
## out$patients = unique(patient_data) | |
## } | |
## if ("group_id" %in% names(self)) | |
## out$groups = data.frame(group_id = unique(self$group_id)) | |
## if ("time_start" %in% names(self)) | |
## out$time_start = data.frame(time_start = unique(self$time_start)) | |
## structure(out, class="hesim_data") | |
## } | |
## make_default_utilities = function(self, ...) { | |
## stopifnot(inherits(self, "IndivCtstmTrans"), | |
## hasName(self, "input_data")) | |
## n_states = nrow(self$trans_mat) | |
## utilities_tbl = stateval_tbl(data.frame(state_id=1:n_states, | |
## est=rep(1,n_states)), | |
## dist="fixed") | |
## create_StateVals(utilities_tbl, hesim_data=unexpand(self$input_data), ...) | |
## } | |
## unexpand(transmod$input_data) | |
## make_default_utilities(transmod, n=1)$sim(10) | |
test = C_cohort_ctstm_sim(transmod, | |
NULL, | |
new.env(), | |
start_state=rep(0L,nrow(patients)), | |
start_age=rep(0,nrow(patients)), | |
times=c(0,1,2,10), | |
clock="mixt", | |
transition_types=c(0L,0L,0L,0L), | |
dr_costs=0.03, | |
dr_qalys=0.03) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment