Master-Equation Solvers
These optional numerical methods operate on RNA probability distributions. They do not replace the default steady-state RNA fitter, HMM transition solver, or MCMC sampler.
All rates are per minute; times are minutes. Joint distributions use P[configuration, m + 1] for RNA count m, so Julia's vec(P) has the configuration index varying fastest. Finite generators have column sums zero.
Transient RNA Distributions
using StochasticGene
transitions = ([1, 2], [2, 1])
components = StochasticGene.MComponents(transitions, 2, 0, 60, 1.0, "")
rates = [0.3, 0.5, 8.0] # k12, k21, production; decay is in components
problem = transient_master_problem(components, rates)
P0 = transient_master_initial([0.625, 0.375], 60)
P = transient_master_closure(problem, P0, 1.0; ode_steps=200)
histogram = transient_master_marginal(P)transient_master_problem supports production that leaves the finite configuration unchanged. For GR/GRSM ejection events, which create RNA and change configuration, use transient_master_problem_general(components, rates). The original adapter continues to reject that case explicitly. Both component adapters require single-transcript ejection and scalar linear decay. They reject count-burst operators instead of treating multiple-transcript events as single births. Direct construction can be used with an output window of one bin; the component adapter needs at least two bins to recover decay.
For direct construction, TransientMasterProblemAB(A, B, decay) describes nonproductive transitions A and single-transcript productive transitions B. A + B must be a finite generator. The count equations are
dP_m/dt = A*P_m + B*P_(m-1) + decay*((m+1)*P_(m+1) - m*P_m).| Method | Production | Step control | Notes |
|---|---|---|---|
transient_master_strang | Diagonal B | positional n_steps | Existing second-order split |
transient_master_strang_richardson | Diagonal B | positional base steps | Extrapolated fourth order when window loss is negligible; may give negative entries |
transient_master_strang_purebd | General B | positional n_steps | Death split plus RK4 production/configuration step |
transient_master_closure | General B | ode_steps | RK4 multiplier ODE, no operator-splitting error |
transient_master_closure_taylor | General B | n_steps, R | Explicit order-R multiplier integration |
transient_master_closure_exp_taylor | General B | n_steps, R | Exponential treatment of the constant generator; Taylor treatment of the remaining term |
The closure methods use a generating-function composition/multiplier representation. Their M_out is an output window, not a reflecting cap: probability outside it is not added to the last bin or renormalized. P0 can have a larger count support than the output window. Any initial mass omitted from P0 is, of course, unavailable to the solver.
Splitting methods instead discard mass outside their represented count window at intermediate steps. Death can otherwise return that mass to lower counts. Check both count-window and time-step convergence; these are different errors. Comparisons to the existing capped full CME require negligible cap probability or a deliberately larger reference window.
All listed time integrators are fixed-step. Defaults are convenience choices, not error-tolerance guarantees. Double step counts and compare results; inspect nonnegativity and mass without silently clipping or normalizing away errors. The new solvers reject nonfinite results, materially negative probabilities, or a material increase over the supplied initial mass, asking for smaller steps. Passing those checks does not establish accuracy. Richardson extrapolation is deliberately not clipped and can return negative entries. An exponential treatment of one term does not guarantee stability of the full scheme at arbitrarily large steps. Taylor orders are restricted to 1 through
- The new closure/state-decay methods use Float64 work arrays; they are not
gradient-based inference interfaces. No universal performance ranking is claimed.
State-Dependent Decay
TransientMasterProblemStateMu(A, B, decay_vector) allows one decay rate per finite configuration, including zero.
transient_master_strang_statewise(problem, P0, t, n_steps)handles diagonal productiveB. It rejects off-diagonal productive transitions rather than dropping them.transient_master_strang_shifted(problem, P0, t, n_steps; mu0=minimum(decay_vector))also handles generalB. It splits off a reference scalar decay and a residual state-dependent death operator. Require0 <= mu0 <= minimum(decay_vector)so the residual is a valid death process. It uses a dense joint matrix exponential and is intended for moderate joint dimensions, not large coupled-state spaces.
Optional Steady-State Block Solver
M = StochasticGene.make_mat_M(components, rates)
p = StochasticGene.steady_state_vector(M;
solver=:closure_thomas, nT=components.nT)
h = StochasticGene.steady_state(M, components.nT, 2;
steady_state_solver=:closure_thomas) # two alleles, existing convolutionThe block-Thomas method exploits the count-level block-tridiagonal structure of the existing capped generator. It preserves that boundary convention; it is not the transient closure's uncapped output window. It validates matrix layout and rejects unsupported structures, including nonstandard count jumps and state-dependent decay. Singular elimination falls back to the existing robust nullspace solver. The default solver is unchanged.
ForwardDiff through supported nonsingular elimination is tested. No reverse-mode rule is added: retain solver=:augmented for the existing reverse-mode path. Benchmark on the actual model before choosing a solver; dense finite-state blocks can be expensive for large coupled configurations.
API
StochasticGene.TransientMasterProblem — Type
TransientMasterProblem(T, beta, decay)Finite-modulator transient RNA problem for the split master equation.
T is the finite configuration generator with column sums zero, beta[a] is the mature-RNA production rate in configuration a, and decay is the mature-RNA decay rate per molecule. The joint distribution is stored as P[a, m+1].
This problem type represents the affine production/linear-decay case used by transient_master_strang. It is intentionally separate from the existing steady-state CME matrix machinery.
StochasticGene.TransientMasterProblemAB — Type
TransientMasterProblemAB(A, B, decay)Transient RNA problem with general (possibly off-diagonal) productive transitions.
A is the part of the finite-configuration generator that does not create transcripts, B is the part that creates one transcript per transition, and A + B is the full finite generator (column-sum zero, nonnegative off-diagonal). decay is the mature-RNA decay rate. Off-diagonal entries B[i, j] with i != j correspond to transitions that both change the finite configuration and produce one transcript (e.g. the R-step ejection of GR/GRSM models).
Use transient_master_closure or transient_master_strang_purebd to evolve this problem. The diagonal-production case can also be expressed as a TransientMasterProblem, which is the lower-overhead form used by the existing splitting routines.
StochasticGene.TransientMasterProblemStateMu — Type
TransientMasterProblemStateMu(A, B, decay)Transient RNA problem with state-dependent mature-RNA decay. decay[a] is the decay rate in finite configuration a. The non-productive and productive finite-state generators are A and B as in TransientMasterProblemAB.
StochasticGene.transient_master_problem — Function
transient_master_problem(components::MComponents, rates)Build a TransientMasterProblem from existing MComponents.
This adapter uses the existing transition-matrix machinery to build the finite configuration generator T and productive-event matrix B. It currently supports the diagonal-production case where B has no off-diagonal entries, so production increases mature RNA but leaves the finite configuration unchanged. If B contains off-diagonal productive transitions, the exact closed-form A-flow used here is not valid and this method throws an ArgumentError.
Important: GR/GRSM models with R > 0 usually have a last-R-step ejection transition that both changes the finite R configuration and increments mature mRNA. That marked transition is represented by off-diagonal entries in B. It is therefore detected here and rejected rather than silently approximated with the diagonal-production closure. Supporting that case requires a generalized marked-transition A-flow or a fallback to the full joint transient CME.
StochasticGene.transient_master_problem_general — Function
transient_master_problem_general(components::MComponents, rates)Build a TransientMasterProblemAB from existing MComponents, allowing off-diagonal productive transitions. This is the general analogue of transient_master_problem, which only handles diagonal B.
StochasticGene.transient_master_initial — Function
transient_master_initial(config_distribution, nhist; m0=0)Create an nT × nhist joint initial distribution concentrated at RNA count m0 with finite-configuration probabilities config_distribution.
StochasticGene.transient_master_strang — Function
transient_master_strang(problem, P0, t_final, n_steps; return_joint=true)Evolve a transient RNA distribution using Strang splitting: A(dt/2) B(dt) A(dt/2).
P0 is an nT × nhist joint distribution with P0[a, m+1]. When return_joint=false, returns the marginal mature-RNA histogram instead.
transient_master_strang(problem, P0, times; steps_per_interval=1, return_joint=true)Evolve through increasing times and return one snapshot per time. The initial distribution P0 is interpreted at time zero.
StochasticGene.transient_master_strang_richardson — Function
transient_master_strang_richardson(problem, P0, t_final, n_steps_base; return_joint=true)Richardson extrapolation of transient_master_strang at step counts n_steps_base and 2 * n_steps_base. The leading Strang Delta-t^2 commutator term cancels, leaving Delta-t^4 error at roughly three times the cost of a single base-step Strang run. This is the dissipative-semigroup-safe higher-order upgrade without negative-time substeps. Extrapolation itself does not preserve nonnegativity: inspect the result and refine the step count rather than clipping negative probabilities. Its fourth-order rate assumes count-window loss is negligible; increasing the count window is a separate convergence check.
StochasticGene.transient_master_strang_purebd — Function
transient_master_strang_purebd(problem, P0, t_final, n_steps; return_joint=true)Strang split with L_A = pure mRNA degradation (per-state binomial thinning, closed form) and L_B = finite-configuration transitions plus production (lower-triangular in mRNA index, RK4 on the truncated system). Unlike the diagonal-B Strang of transient_master_strang, production may couple configurations arbitrarily, so this splitting handles the GR/GRSM ejection case. The death half-step is exact for its represented input; the production half-step uses RK4. Probability that leaves the represented count window is discarded at intermediate steps and cannot later return by decay. Enlarge the count window separately from refining n_steps. This differs from the output-window semantics of transient_master_closure.
StochasticGene.transient_master_closure — Function
transient_master_closure(problem, P0, t_final; M_out, ode_steps, return_joint=true)Evolve a transient RNA distribution by the matrix-valued composition/multiplier closure: exact (up to ODE error) in-window solver with no operator-splitting error. Works on either TransientMasterProblem or TransientMasterProblemAB; the latter is required when productive transitions are off-diagonal.
The closure integrates a lower-triangular matrix ODE for the multipliers K[m](t) on m = 0:M_out, composes the initial distribution with binomial thinning by survival probability exp(-decay * t_final), and applies the multipliers by Cauchy convolution. The mRNA dynamics are not truncated during propagation; truncation only enters through the requested output window and through any tail mass omitted from P0.
ode_steps controls the fixed RK4 step count; the default scales with t_final, not the rates. Check convergence by increasing it, especially for stiff rates. The matrix ODE uses batched GEMM. New closure variants use Float64 work arrays and are not automatic-differentiation entry points.
StochasticGene.transient_master_closure_taylor — Function
transient_master_closure_taylor(problem, P0, t_final;
M_out, n_steps, R=8, return_joint=true)Order-R Taylor integration of the multiplier ODE in place of the RK4 path used by transient_master_closure. R must be between 1 and 20. This is a fixed-step explicit scheme: verify step convergence and positivity. Performance relative to RK4 depends on rates, state count and tolerance.
StochasticGene.transient_master_closure_exp_taylor — Function
transient_master_closure_exp_taylor(problem, P0, t_final;
M_out, n_steps, R=8, return_joint=true)Exponential-Taylor integration of the multiplier ODE: matrix exponential on the constant linear part L = A + B and order-R Taylor on the time-varying coupling p(t) B (K[m-1] - K[m]). The constant generator is handled by a matrix exponential; the time-varying term remains a finite Taylor expansion. This is not an unconditional stability or accuracy guarantee for stiff problems. Verify step convergence. R must be between 1 and 20. The augmented-exponential phi-function bundle is computed once per solve on the uniform grid.
StochasticGene.transient_master_strang_statewise — Function
transient_master_strang_statewise(problem, P0, t_final, n_steps; return_joint=true)Strang split for TransientMasterProblemStateMu: per-state mRNA birth/death (closed form, with state-specific decay) sandwiched around a full-step exp((A + B) * dt) on the finite-state generator. Valid when B is diagonal (production does not change configuration); the productive transitions contribute via beta[a] = B[a, a].
StochasticGene.transient_master_strang_shifted — Function
transient_master_strang_shifted(problem, P0, t_final, n_steps; mu0, return_joint=true)Strang split for TransientMasterProblemStateMu using a uniform reference decay mu0: a closure half-step at mu0 is sandwiched around a dense matrix-exponential step on the residual (decay[a] - mu0) m P_a(m) correction. The residual operator is block-diagonal in finite configuration (only acts on the mRNA index), so the matrix exponential costs O((n_T (M+1))^3) once per step (amortizable across uniform grids). This is only viable for moderate joint dimensions; for large joint state spaces, prefer the statewise splitting.
StochasticGene.transient_master_marginal — Function
transient_master_marginal(P)Marginalize a configuration-by-RNA-count transient master distribution over finite promoter configurations.
StochasticGene.steady_state_vector — Function
steady_state_vector(M::AbstractMatrix; solver::Symbol=:default)Return a normalized steady-state vector p with M * p ≈ 0 and entries summing to 1.
solver=:defaultor:fast—normalized_nullspace(sparse QR, SVD fallback).solver=:augmentedor:autodiff—normalized_nullspace_augmented(linear solve + AD rules).solver=:closure_thomaswithnT: optional block solver for the standard single-transcript RNA generator; validates the layout before elimination.
Non-sparse M is converted with sparse(M).
StochasticGene.normalized_nullspace — Function
normalized_nullspace(M::SparseMatrixCSC)
Default steady-state solver. Tries the fast QR-based method first and falls back to SVD if the QR path produces a non-finite or ill-normalized vector.
StochasticGene.normalized_nullspace_augmented — Function
normalized_nullspace_augmented(M::SparseMatrixCSC)Compute a steady-state vector p with M * p ≈ 0 and sum(p) == 1 by solving the n×n linear system formed from the first n-1 rows of M and the constraint sum(p) = 1. This avoids sparse QR pivoting and SVD, so it is suitable for forward-mode AD (e.g. ForwardDiff through the linear solve; can be fragile for sparse \). Reverse mode: ChainRulesCore.rrule is defined for this function, so Zygote-style AD will use the hand-coded pullback. You can also call pullback_normalized_nullspace_augmented directly.
The last row of M is not used; for a CTMC generator with row sums zero, that row is linearly dependent on the others.
Output is clipped and renormalized like normalized_nullspace: negative entries from roundoff are projected with max(·,0) and the vector is rescaled to sum to 1. If the augmented system is singular or non-finite, falls back to normalized_nullspace_svd for Float32/Float64, or dense normalized_nullspace_qr for ForwardDiff.Dual (SVD is unavailable). pullback_normalized_nullspace_augmented errors on any fallback.
See also
normalized_nullspace— fast QR default for plainFloat64use.
StochasticGene.normalized_nullspace_svd — Function
normalizednullspacesvd(M::SparseMatrixCSC)
Robust nullspace via SVD. Computes the right singular vector associated with the smallest singular value and normalizes it. More expensive than QR but numerically safer; useful as a fallback and for benchmarking.
StochasticGene.normalized_nullspace_qr — Function
normalized_nullspace_qr(M::SparseMatrixCSC)Fast QR-based nullspace used historically in this package. Assumes an irreducible rate matrix of rank n-1; may be fragile when the matrix is close to singular beyond rank n-1.
Implementation avoids in-place setindex! so reverse-mode AD (Zygote) can differentiate when this path is used; for gradients prefer normalized_nullspace_augmented with steady_state_solver=:augmented.
StochasticGene.pullback_normalized_nullspace_augmented — Function
pullback_normalized_nullspace_augmented(M::SparseMatrixCSC, ȳ::AbstractVector)
pullback_normalized_nullspace_augmented(M, ȳ, r)Hand-coded reverse-mode map for the augmented steady-state: returns primal p = normalized_nullspace_augmented(M) when the augmented linear system succeeds, and sparse M̄ with the same structure as M, representing the gradient of dot(ȳ, p) with respect to the entries of M (only rows 1:n-1 receive nonzero sensitivities; the last row of M is unused in the augmented formulation).
If the augmented solve would fall back to SVD, throws ArgumentError (call normalized_nullspace for a primal-only robust path).
The three-argument form reuses the named tuple r from _augmented_nullspace_try so a ChainRulesCore.rrule can avoid repeating the forward solve.
Use this from ChainRulesCore.rrule, Zygote @adjoint, or a custom optimizer.
StochasticGene._augmented_nullspace_try — Function
_augmented_nullspace_try(M, n) -> Union{Nothing, NamedTuple}On success, returns (; out, A, p, s) where out is the normalized steady state, A and p satisfy A * p = b with b = [0…0, 1], and s = sum(max.(p,0)). Returns nothing if the augmented system fails (singular / non-finite).
StochasticGene.normalized_nullspace_closure_thomas — Function
normalized_nullspace_closure_thomas(M::SparseMatrixCSC, nT::Int)Steady-state vector of the truncated joint generator M via block-Thomas elimination on the closure recurrence. nT is the hidden-state dimension (the block size). The returned vector is in the same m-major linear layout as M, with nonnegative entries and unit total mass.
The reflective boundary correction that make_mat_M installs at the cap is honored: the cap block uses (A + B) - mu * Mcap * I, matching the existing augmented-LU steady state.
Forward-mode AD through ForwardDiff.Dual works out of the box (every step is a dense n_T x n_T LU); a reverse-mode rrule is not yet provided, so Zygote-style gradients should continue to use the :augmented solver.
StochasticGene.make_mat_M — Function
make_mat_M(T::SparseMatrixCSC, B::SparseMatrixCSC, decay::Float64, total::Int)
make_mat_M(T::SparseMatrixCSC, B::SparseMatrixCSC, U::SparseMatrixCSC, Uminus::SparseMatrixCSC, Uplus::SparseMatrixCSC)
make_mat_M(components::MComponents, rates::Vector)Return M matrix used to compute steady state RNA distribution.
Description
This function returns the M matrix used to compute the steady state RNA distribution. It can either generate the U, Uminus, and Uplus matrices internally, accept them as arguments, or use components to create the matrix.
Arguments
T::SparseMatrixCSC: Transition matrix.B::SparseMatrixCSC: Boundary matrix.decay::Float64: Decay rate.total::Int: Total number of transitions.U::SparseMatrixCSC: Precomputed U matrix.Uminus::SparseMatrixCSC: Precomputed Uminus matrix.Uplus::SparseMatrixCSC: Precomputed Uplus matrix.components::MComponents: Components structure containing elements and matrices.rates::Vector: Vector of rates corresponding to the elements.
Methods
make_mat_M(T::SparseMatrixCSC, B::SparseMatrixCSC, decay::Float64, total::Int): Generates the U, Uminus, and Uplus matrices internally.make_mat_M(T::SparseMatrixCSC, B::SparseMatrixCSC, U::SparseMatrixCSC, Uminus::SparseMatrixCSC, Uplus::SparseMatrixCSC): Accepts precomputed U, Uminus, and Uplus matrices.make_mat_M(components::MComponents, rates::Vector): Uses components to create the matrix.
Returns
SparseMatrixCSC: The created M matrix.