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
3 changes: 2 additions & 1 deletion Code/Source/solver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ set(CSRCS
active_stress_uniform_unsteady.cpp
active_stress_ode.cpp
active_stress_nash_panfilov.cpp

active_stress_regazzoni.cpp

SPLIT.c

svZeroD_interface/LPNSolverInterface.h svZeroD_interface/LPNSolverInterface.cpp
Expand Down
2 changes: 1 addition & 1 deletion Code/Source/solver/active_stress.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ void ActiveStress::advance_time_step(const double t, const double dt,
fiber_stretch_rate[i], state_loc);
states.set_col(i, state_loc);

active_tension[i] = compute_active_tension_local(state_loc);
active_tension[i] = compute_active_tension_local(state_loc, fiber_stretch[i]);
}
}
6 changes: 5 additions & 1 deletion Code/Source/solver/active_stress.h
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,13 @@ class ActiveStress {

/**
* @brief Compute the active tension for a single node.
*
* @param[in] state State vector for a single node.
* @param[in] fiber_stretch Fiber stretch at the current node.
*/
virtual double
compute_active_tension_local(const Vector<double> &state) const = 0;
compute_active_tension_local(const Vector<double> &state,
const double fiber_stretch) const = 0;

/// Current time. Updated whenever calling @ref advance_time_step.
double time;
Expand Down
3 changes: 2 additions & 1 deletion Code/Source/solver/active_stress_nash_panfilov.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ Vector<double> NashPanfilov::getf(const double t, const Vector<double> &state,
}

double
NashPanfilov::compute_active_tension_local(const Vector<double> &state) const {
NashPanfilov::compute_active_tension_local(const Vector<double> &state,
const double fiber_stretch) const {
return state[0];
}

Expand Down
3 changes: 2 additions & 1 deletion Code/Source/solver/active_stress_nash_panfilov.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ class NashPanfilov : public ActiveStressODE {
* @brief Compute the active tension for a single node.
*/
virtual double
compute_active_tension_local(const Vector<double> &state) const override;
compute_active_tension_local(const Vector<double> &state,
const double fiber_stretch) const override;

/// @name Model parameters.
/// @{
Expand Down
4 changes: 2 additions & 2 deletions Code/Source/solver/active_stress_ode.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* \dv{\astressstate}{t} &=
* \mathbf{F}_\text{AS}(t, \astressstate, \calcium, \fiberstretch,
* \fiberstretchrate)\;, \\
* \Tact &= \Tact(\astressstate)\;.
* \Tact &= \Tact(\astressstate, \fiberstretch)\;.
* \end{aligned} @f]
*
* ### Numerical methods
Expand All @@ -25,7 +25,7 @@
* @ref ODESolver. After that, the active tension is computed for every node
* @f$i@f$ as:
* @f[
* {\Tact}_{i}^{n+1} = \Tact(\astressstate_i^{n+1})\;.
* {\Tact}_{i}^{n+1} = \Tact(\astressstate_i^{n+1}, \fiberstretch_i^{n+1})\;.
* @f]
*
* ### Implementing derived models
Expand Down
315 changes: 315 additions & 0 deletions Code/Source/solver/active_stress_regazzoni.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the
// University of California, and others. SPDX-License-Identifier: BSD-3-Clause

#include "active_stress_regazzoni.h"

#include "eigen3/Eigen/Dense"

#include <algorithm>
#include <cmath>

void RegazzoniActiveStress::read_model_specific_parameters(
const ActiveStressModelParameters &params) {
Kbasic = params.get_scalar("Kbasic");
Koff = params.get_scalar("Koff");
Q = params.get_scalar("Q");
mu = params.get_scalar("mu");
gamma = params.get_scalar("gamma");
Kd0 = params.get_scalar("Kd0");
alphaKd = params.get_scalar("alphaKd");
if (alphaKd > 0.0)
svmp::raise<svmp::ParseException>(
"RegazzoniActiveStress: alphaKd must be <= 0 (positive values reduce calcium "
"sensitivity with stretch, reversing length-dependent activation, "
"and can produce a zero dissociation constant at physiological "
"sarcomere lengths).");
SL0 = params.get_scalar("SL0");
ru_substep = params.get_scalar("ru_substep");
kd_reference_sarcomere_length = params.get_scalar("kd_reference_sarcomere_length");

r0 = params.get_scalar("r0");
alpha = params.get_scalar("alpha");
mu0_fP = params.get_scalar("mu0_fP");
mu1_fP = params.get_scalar("mu1_fP");

LA = params.get_scalar("LA");
LM = params.get_scalar("LM");
LB = params.get_scalar("LB");
a_XB = params.get_scalar("a_XB");
}

void RegazzoniActiveStress::distribute_model_specific_parameters(
const CmMod &cm_mod, const cmType &cm) {
cm.bcast(cm_mod, &Kbasic);
cm.bcast(cm_mod, &Koff);
cm.bcast(cm_mod, &Q);
cm.bcast(cm_mod, &mu);
cm.bcast(cm_mod, &gamma);
cm.bcast(cm_mod, &Kd0);
cm.bcast(cm_mod, &alphaKd);
cm.bcast(cm_mod, &SL0);
cm.bcast(cm_mod, &ru_substep);
cm.bcast(cm_mod, &kd_reference_sarcomere_length);

cm.bcast(cm_mod, &r0);
cm.bcast(cm_mod, &alpha);
cm.bcast(cm_mod, &mu0_fP);
cm.bcast(cm_mod, &mu1_fP);

cm.bcast(cm_mod, &LA);
cm.bcast(cm_mod, &LM);
cm.bcast(cm_mod, &LB);
cm.bcast(cm_mod, &a_XB);
}

void RegazzoniActiveStress::init_local(Vector<double> &state) const {
for (unsigned int i = 0; i < n_state_variables; ++i)
state[i] = 0.0;

state[ru_index(0, 0, 0, 0)] = 1.0; // == state[0]
}

void RegazzoniActiveStress::advance_time_step_local(
const double t, const double dt, const double calcium,
const double fiber_stretch, const double fiber_stretch_rate,
Vector<double> &state) const {
const double sarcomere_length = SL0 * fiber_stretch;

// Calcium/stretch-independent central-tropomyosin transition rates.
const RUArray rates_T = ru_transition_rates_tropomyosin();

// Troponin transition rates rates_C[CC][TC]: the calcium-binding row (CC = 0)
// depends on calcium and sarcomere length; the unbinding row (CC = 1) does
// not.
const double calcium_on_rate =
Koff /
(Kd0 - alphaKd * (kd_reference_sarcomere_length - sarcomere_length)) *
calcium;
BinaryPairArray rates_C;
rates_C[0][0] = calcium_on_rate;
rates_C[0][1] = calcium_on_rate;
rates_C[1][0] = Koff;
rates_C[1][1] = Koff / mu;

// Deserialize the 16 RU probabilities (entries 0-15). The crossbridge moments
// (entries 16-19) are left untouched by this increment.
RUArray state_RU;
for (int TL = 0; TL < 2; ++TL)
for (int TC = 0; TC < 2; ++TC)
for (int TR = 0; TR < 2; ++TR)
for (int CC = 0; CC < 2; ++CC)
state_RU[TL][TC][TR][CC] = state[ru_index(TL, TC, TR, CC)];

// Forward-Euler substepping over the outer time step. The final substep is
// shortened so that the outer step is covered exactly.
double time_advanced = 0.0;
while (time_advanced <= dt - 1.0e-10) {
const double substep = std::min(ru_substep, dt - time_advanced);
ru_forward_euler_substep(substep, rates_T, rates_C, state_RU);
time_advanced += substep;
}

// Advance the crossbridge moments (entries 16-19) from the updated RU state.
// The velocity v = -dSL/dt / SL0 reduces to -d(lambda)/dt because SL = SL0 * lambda.
const double velocity = -fiber_stretch_rate;
XBArray state_XB;
for (int i = 0; i < 4; ++i)
state_XB[i] = state[xb_index(i)];
state_XB = xb_implicit_update(dt, velocity, rates_T, state_RU, state_XB);

// Serialize the updated RU probabilities back into the state vector.
for (int TL = 0; TL < 2; ++TL)
for (int TC = 0; TC < 2; ++TC)
for (int TR = 0; TR < 2; ++TR)
for (int CC = 0; CC < 2; ++CC)
state[ru_index(TL, TC, TR, CC)] = state_RU[TL][TC][TR][CC];

// Serialize the updated crossbridge moments back into the state vector.
for (int i = 0; i < 4; ++i)
state[xb_index(i)] = state_XB[i];
}

double RegazzoniActiveStress::compute_active_tension_local(
const Vector<double> &state, const double fiber_stretch) const {
const double sarcomere_length = SL0 * fiber_stretch;

// Active tension T_act = a_XB * (μ_P^1 + μ_N^1) * φ(SL) from the
// permissive and non-permissive XB first moments (state entries 17 and 19),
// scaled by the single-overlap fraction and the upscaling factor a_XB.
return a_XB * (state[xb_index(1)] + state[xb_index(3)]) *
fraction_single_overlap(sarcomere_length);
}

RegazzoniActiveStress::RUArray
RegazzoniActiveStress::ru_transition_rates_tropomyosin() const {
RUArray rates_T;
for (int TL = 0; TL < 2; ++TL)
for (int TR = 0; TR < 2; ++TR) {
const int permissive_neighbors = TL + TR;

// Rate of leaving the permissive central state (TC = 1).
const double closing_rate =
Kbasic * std::pow(gamma, 2 - permissive_neighbors);
// Rate of leaving the non-permissive central state (TC = 0).
const double opening_rate =
Q * Kbasic * std::pow(gamma, permissive_neighbors);

rates_T[TL][1][TR][0] = closing_rate;
rates_T[TL][1][TR][1] = closing_rate;
rates_T[TL][0][TR][0] = opening_rate / mu;
rates_T[TL][0][TR][1] = opening_rate;
}
return rates_T;
}

void RegazzoniActiveStress::ru_forward_euler_substep(
double dt, const RUArray &rates_T,
const BinaryPairArray &rates_C, RUArray &state_RU) const {
// Probability fluxes from central-unit transitions.
RUArray flux_TC; // central tropomyosin
RUArray flux_CC; // central troponin
for (int TL = 0; TL < 2; ++TL)
for (int TC = 0; TC < 2; ++TC)
for (int TR = 0; TR < 2; ++TR)
for (int CC = 0; CC < 2; ++CC) {
flux_TC[TL][TC][TR][CC] =
state_RU[TL][TC][TR][CC] * rates_T[TL][TC][TR][CC];
flux_CC[TL][TC][TR][CC] =
state_RU[TL][TC][TR][CC] * rates_C[CC][TC];
}

// Effective transition rates of the boundary neighbours, obtained from the
// mean-field closure by conditioning the central-unit flux on the neighbour
// pair state.
BinaryPairArray rate_left;
BinaryPairArray rate_right;
for (int TL = 0; TL < 2; ++TL)
for (int TC = 0; TC < 2; ++TC) {
double flux_sum = 0.0;
double prob_sum = 0.0;
for (int TR = 0; TR < 2; ++TR)
for (int CC = 0; CC < 2; ++CC) {
flux_sum += flux_TC[TL][TC][TR][CC];
prob_sum += state_RU[TL][TC][TR][CC];
}
rate_left[TL][TC] = (prob_sum > 1.0e-12) ? flux_sum / prob_sum : 0.0;
}
for (int TR = 0; TR < 2; ++TR)
for (int TC = 0; TC < 2; ++TC) {
double flux_sum = 0.0;
double prob_sum = 0.0;
for (int TL = 0; TL < 2; ++TL)
for (int CC = 0; CC < 2; ++CC) {
flux_sum += flux_TC[TL][TC][TR][CC];
prob_sum += state_RU[TL][TC][TR][CC];
}
rate_right[TR][TC] = (prob_sum > 1.0e-12) ? flux_sum / prob_sum : 0.0;
}

// Probability fluxes from the boundary-neighbour transitions.
// TL's only neighbour is TC on its right → rate_right[TC][TL].
// TR's only neighbour is TC on its left → rate_left[TC][TR].
// (rate_left == rate_right numerically due to mean-field LR symmetry, so the
// result is unchanged, but the names now match the physical convention.)
RUArray flux_TL; // left tropomyosin
RUArray flux_TR; // right tropomyosin
for (int TL = 0; TL < 2; ++TL)
for (int TC = 0; TC < 2; ++TC)
for (int TR = 0; TR < 2; ++TR)
for (int CC = 0; CC < 2; ++CC) {
flux_TL[TL][TC][TR][CC] =
state_RU[TL][TC][TR][CC] * rate_right[TC][TL];
flux_TR[TL][TC][TR][CC] =
state_RU[TL][TC][TR][CC] * rate_left[TC][TR];
}

// Forward-Euler update of the 16 RU probabilities.
for (int TL = 0; TL < 2; ++TL)
for (int TC = 0; TC < 2; ++TC)
for (int TR = 0; TR < 2; ++TR)
for (int CC = 0; CC < 2; ++CC)
state_RU[TL][TC][TR][CC] +=
dt * (-flux_TL[TL][TC][TR][CC] + flux_TL[1 - TL][TC][TR][CC] -
flux_TC[TL][TC][TR][CC] + flux_TC[TL][1 - TC][TR][CC] -
flux_TR[TL][TC][TR][CC] + flux_TR[TL][TC][1 - TR][CC] -
flux_CC[TL][TC][TR][CC] + flux_CC[TL][TC][TR][1 - CC]);
}

RegazzoniActiveStress::XBArray RegazzoniActiveStress::xb_implicit_update(
double dt, double velocity,
const RUArray &rates_T,
const RUArray &state_RU,
const XBArray &state_XB) const {
// Permissivity and the permissive/non-permissive probability fluxes from the
// updated RU state.
double permissivity = 0.0;
double flux_PN = 0.0;
double flux_NP = 0.0;
for (int TL = 0; TL < 2; ++TL)
for (int TR = 0; TR < 2; ++TR)
for (int CC = 0; CC < 2; ++CC) {
permissivity += state_RU[TL][1][TR][CC];
flux_PN += state_RU[TL][1][TR][CC] * rates_T[TL][1][TR][CC];
flux_NP += state_RU[TL][0][TR][CC] * rates_T[TL][0][TR][CC];
}

// Effective permissive->non-permissive and non-permissive->permissive rates.
const double k_PN = (permissivity >= 1.0e-12) ? flux_PN / permissivity : 0.0;
const double k_NP =
((1.0 - permissivity) >= 1.0e-12) ? flux_NP / (1.0 - permissivity) : 0.0;

// Use the calibrated RDQ20-MF specialization of the general XB system:
// new XBs attach only in the permissive state (f_N = 0), and both XB
// populations share r(v) = r0 + alpha * |v|. Non-permissive moments
// are populated by P-to-N transitions of already-attached XBs.
const double r = r0 + alpha * std::abs(velocity);
const double diag_P = r + k_PN;
const double diag_N = r + k_NP;

// Implicit-Euler system (I - dt * A) x = rhs for the four moments. The matrix
// is zero-initialized so the structurally-zero entries are correct.
Eigen::Matrix<double, 4, 4> system = Eigen::Matrix<double, 4, 4>::Zero();
system(0, 0) = -diag_P;
system(1, 1) = -diag_P;
system(2, 2) = -diag_N;
system(3, 3) = -diag_N;
system(0, 2) = k_NP;
system(1, 3) = k_NP;
system(2, 0) = k_PN;
system(3, 1) = k_PN;
system(1, 0) = -velocity;
system(3, 2) = -velocity;
system *= -dt;
for (int i = 0; i < 4; ++i)
system(i, i) += 1.0;

Eigen::Matrix<double, 4, 1> rhs;
rhs(0) = state_XB[0] + dt * permissivity * mu0_fP;
rhs(1) = state_XB[1] + dt * permissivity * mu1_fP;
rhs(2) = state_XB[2];
rhs(3) = state_XB[3];

const Eigen::Matrix<double, 4, 1> solution =
system.colPivHouseholderQr().solve(rhs);
XBArray result;
for (int i = 0; i < 4; ++i)
result[i] = solution(i);
return result;
}

double RegazzoniActiveStress::fraction_single_overlap(double sarcomere_length) const {
const double SL = sarcomere_length;
const double half_single_overlap = (LM - LB) * 0.5;

if (SL > LA && SL <= LM)
return (SL - LA) / half_single_overlap;
if (SL > LM && SL <= 2.0 * LA - LB)
return (SL + LM - 2.0 * LA) * 0.5 / half_single_overlap;
if (SL > 2.0 * LA - LB && SL <= 2.0 * LA + LB)
return 1.0;
if (SL > 2.0 * LA + LB && SL <= 2.0 * LA + LM)
return (LM + 2.0 * LA - SL) * 0.5 / half_single_overlap;
return 0.0;
}

REGISTER_ACTIVE_STRESS_MODEL("Regazzoni", RegazzoniActiveStress);
Loading
Loading