1function [S, SS, dpi, pi] = getSensitivity(self, param, reward)
2% [S, SS, DPI, PI] = GETSENSITIVITY(PARAM, REWARD)
4% Parametric sensitivity of a steady-state reward to a scalar model
5% parameter, following Trivedi and Bobbio (2017), Sec. 9.7.
7% PARAM describes
the parameter theta and how to set it on
the model:
8% param.name identifier used in reports
9% param.value nominal value theta
10% param.set handle (model, value) -> void, applying theta to
the model
11% param.step optional finite-difference step,
default value*1e-6
13% The generator derivative dQ/dtheta
is obtained by central differences on
14%
the rate with
the state space held fixed. This
is exact to O(step^2) and
15%
requires no symbolic differentiation of
the rate assembly;
the state space
16%
is unaffected because it depends on
the topology and
the cutoff, not on
17% rate values. The steady-state sensitivity then follows from one linear
18% solve, see ctmc_sens.
20% REWARD
is either a function handle mapping
the state space to a reward rate
21% vector, or a numeric reward rate vector over
the states. If omitted, dpi
is
22% returned and S
is empty.
24% Note:
this returns d(E[r])/dtheta with dr/dtheta = 0, i.e. it assumes
the
25% reward rates
do not themselves depend on theta. Rewards that depend on
26% theta need
the second term of Eq. (9.83) and are not handled here.
28% @param param Struct describing
the parameter, see above
29% @param reward Reward rate vector or handle over
the state space (optional)
30% @
return S Unscaled sensitivity d(E[r])/dtheta, Eq. (9.79)
31% @
return SS Scaled sensitivity (theta/E[r]) d(E[r])/dtheta, Eq. (9.80)
32% @
return dpi Sensitivity of
the steady-state distribution (1 x n)
33% @
return pi Steady-state distribution (1 x n)
35% Copyright (c) 2012-2026, Imperial College London
38if ~isstruct(param) || ~isfield(param,
'set') || ~isfield(param,
'value')
39 line_error(mfilename, 'param must be a struct with fields value and set');
46if isfield(param, 'step') && ~isempty(param.step)
49 h = max(abs(theta), 1) * 1e-6;
52% Nominal generator and state space
53[Q, ~, ~] = self.getGenerator();
55space = self.getStateSpace();
58% Central differences on theta with
the state space fixed
59Qp = perturbedGenerator(self, param, theta + h);
60Qm = perturbedGenerator(self, param, theta - h);
61if size(Qp, 1) ~= n || size(Qm, 1) ~= n
62 line_error(mfilename, ['Perturbing
the parameter changed
the state space size, so
the ', ...
63 'generators cannot be differenced. This happens when
the parameter switches a ', ...
64 'transition on or off (e.g. a zero rate or an immediate transition).']);
66dQ = (Qp - Qm) / (2 * h);
69dpi = ctmc_sens(Q, dQ, pi);
77if isa(reward, 'function_handle')
84 line_error(mfilename, 'reward must have one entry per state');
87% Eq. (9.83) with dr/dtheta = 0
90if abs(Er) > GlobalConstants.Zero
91 SS = (theta / Er) * S;
97function Q = perturbedGenerator(self, param, value)
98% Q = PERTURBEDGENERATOR(SELF, PARAM, VALUE)
99% Rebuild
the generator with theta set to VALUE, on a copy of
the model so
100%
the caller's model
is left untouched.
102% The hard refresh
is required, not defensive: setService and setArrival
103% deliberately leave
the cached struct in place, so a copy that inherited a
104% built struct would report
the old rate and
the difference quotient would
105% silently come out as zero.
107modelCopy = self.model.copy();
108param.set(modelCopy, value);
109modelCopy.refreshStruct(true);
110solverCopy = SolverCTMC(modelCopy, self.getOptions());
111Q = full(solverCopy.getGenerator());