1function [rhs, vars, sys] = getSymbolicDrift(self, options)
2% [RHS, VARS, SYS] = GETSYMBOLICDRIFT(OPTIONS)
4% Right-hand side of the mean-field ODE system as expression strings, one per
5% state variable, together with the variable names they are written in.
7% This
is the input the computer algebra backend needs to produce a Jacobian
8% or an equilibrium (see getJacobian), and it
is the same system
9% solver_fluid_symodes describes and exportODEs typesets, written out
10% variable by variable instead of in matrix
form.
12% ONLY SMOOTH DRIFTS ARE EXPORTED. The
default, matrix, closing and statedep
13% methods scale rates by min(n_i, S_i), which
is not differentiable at
14% n_i = S_i, so their Jacobian does not exist there; emitting a one-sided
15% derivative would be a silent lie exactly at the regime
switch that matters.
16% Use the p-norm smoothing (options.config.pstar, method matrix or pnorm) or
17% the softmin method, whose drifts are smooth everywhere, and
this function
18% refuses the others by name.
20% @param options Solver options (optional, defaults to the solver
's own)
21% @return rhs Cell array of expression strings, one per state variable
22% @return vars Cell array of variable names, x1 ... xn
23% @return sys The structural description from solver_fluid_symodes
25% Copyright (c) 2012-2026, Imperial College London
28if nargin < 2 || isempty(options)
29 options = self.getOptions();
32sys = solver_fluid_symodes(sn, options);
37 vars{s} = sprintf('x%d
', s);
40% see _kb/06-solver-catalog.md for rationale
41eps0 = num2char(GlobalConstants.FineTol);
45 if ~strcmp(sys.smoothing, 'pnorm
')
46 line_error(mfilename, ['The drift of
this method scales rates by min(n_i, S_i),
', ...
47 'which
is not differentiable at n_i = S_i, so it has no Jacobian there.
', ...
48 'Set options.config.pstar to use the p-norm smoothing, or use the
''softmin
'' method.
']);
50 rhs = wform_rhs(sys, vars, eps0);
52 rhs = jform_rhs(sys, vars, eps0);
54 line_error(mfilename, sprintf('unsupported ODE
form ''%s
''', sys.form));
58function rhs = wform_rhs(sys, vars, eps0)
59% dx/dt = W
' * theta(x) + Alambda, with the p-norm smoothed
60% theta_s = x_s / (1 + (n_i/S_i)^p_i)^(1/p_i), theta_s = 0 at a Source,
61% mirroring pnorm_ode in solver_fluid_matrix.
69 i = sys.stateStation(s);
70 ni = stationSum(sys, i, vars, eps0);
76 theta{s} = sprintf('%s/(1 + (%s/%s)^%s)^(1/%s)
', vars{s}, ni, ...
77 num2char(S), num2char(p), num2char(p));
86 if w == 0 || strcmp(theta{t}, '0
')
89 terms{end+1} = sprintf('(%s)*(%s)
', num2char(w), theta{t}); %#ok<AGROW>
91 if sys.Alambda(s) ~= 0
92 terms{end+1} = num2char(sys.Alambda(s)); %#ok<AGROW>
97 rhs{s} = strjoin(terms, ' +
');
102function rhs = jform_rhs(sys, vars, eps0)
103% dx/dt = J * r(x), with r_e = coeff(e) * factor_e(x). Only the smooth factor
104% types are exportable: 'min
' (PS/FCFS under closing and statedep), 'fcfsw
'
105% (statedep FCFS) and 'dpspw
' (piecewise DPS) all carry a min or a branch.
106smooth = {'lin
', 'ext1
', 'dps
', 'fcfsws
'};
107rate = cell(1, sys.nevents);
109 ftype = sys.factorType{e};
110 if ~any(strcmp(ftype, smooth))
111 line_error(mfilename, sprintf(['Event %d scales its rate by the non-smooth
factor ', ...
112 '''%s
'', which has no derivative where the regime switches, so the system has no
', ...
113 'Jacobian. Use the
''softmin
'' method, or the p-norm smoothing of the
''matrix
'' ', ...
114 'method.
'], e, ftype));
116 v = vars{sys.eventVar(e)};
117 fdata = sys.factorData{e};
122 % 1 - sum of the class's phases 2..end at the source
123 if isempty(fdata.others)
126 parts = cell(1, numel(fdata.others));
127 for k = 1:numel(fdata.others)
128 parts{k} = vars{fdata.others(k)};
130 factor = sprintf(
'(1 - (%s))', strjoin(parts,
' + '));
133 % ode_rates_closing seeds the denominator with mean(w) and adds
134 % no FineTol, so neither does this.
135 ntilde = weightedStationSum(sys, fdata.station, sys.dpsw(fdata.station, :), vars,
'0');
136 factor = sprintf(
'%s/(%s + %s)', v, num2char(fdata.c0), ntilde);
139 % ode_softmin: ni
is the raw station total, wni carries FineTol.
140 ni = stationSum(sys, i, vars,
'0');
141 nhat = phaseWeightedStationSum(sys, i, vars, eps0);
142 factor = sprintf(
'%s*(%s)/(%s)', v, softminExpr(ni, num2char(sys.S(i)), sys.alpha), nhat);
144 rate{e} = sprintf(
'(%s)*(%s)', num2char(sys.coeff(e)),
factor);
147rhs = cell(1, sys.nstates);
150 for e = 1:sys.nevents
155 terms{end+1} = sprintf(
'(%s)*(%s)', num2char(j), rate{e}); %#ok<AGROW>
160 rhs{s} = strjoin(terms,
' + ');
165function s = softminExpr(x, y, alpha)
166% Smooth minimum in its weighted-average
form,
167% (x e^{-a x} + y e^{-a y}) / (e^{-a x} + e^{-a y}),
168% which
is what softmin.m computes; softmin.m rewrites it as
169% lo + gap*w/(1+w) only to keep the exponent argument non-positive, an
170% overflow guard that
is meaningless symbolically and would introduce the
171% min/max branch
this export exists to avoid.
173s = sprintf(
'((%s)*exp(-(%s)*(%s)) + (%s)*exp(-(%s)*(%s)))/(exp(-(%s)*(%s)) + exp(-(%s)*(%s)))', ...
174 x, a, x, y, a, y, a, x, a, y);
177function s = stationSum(sys, i, vars, offset)
178% Total fluid mass at station i, plus the offset its consumer uses.
179idx = find(sys.stateStation == i);
180parts = cell(1, numel(idx));
182 parts{k} = vars{idx(k)};
184if strcmp(offset,
'0')
185 s = sprintf('(%s)', strjoin(parts, ' + '));
187 s = sprintf('(%s + %s)', offset, strjoin(parts, ' + '));
191function s = weightedStationSum(sys, i, w, vars, offset)
192% sum_r w_ir * n_ir over the classes of station i (DPS denominator).
195 if sys.stateStation(k) == i
196 wt = w(sys.stateClass(k));
198 parts{end+1} = sprintf(
'(%s)*%s', num2char(wt), vars{k}); %#ok<AGROW>
202s = joinSum(parts, offset);
205function s = phaseWeightedStationSum(sys, i, vars, offset)
206% sum_u w_u x_u over the states of station i, with w_u the mean phase time
207% weights (nhat in ode_softmin).
210 if sys.stateStation(k) == i
211 wt = sys.fcfsPhaseW(k);
213 parts{end+1} = sprintf(
'(%s)*%s', num2char(wt), vars{k}); %#ok<AGROW>
217s = joinSum(parts, offset);
220function s = joinSum(parts, offset)
221% Sum of PARTS, with OFFSET added only when it
is not the literal zero.
223 s = sprintf(
'(%s)', offset);
224elseif strcmp(offset,
'0')
225 s = sprintf('(%s)', strjoin(parts, ' + '));
227 s = sprintf('(%s + %s)', offset, strjoin(parts, ' + '));
231function s = num2char(v)
232% Decimal text the symbolic backend reads as an exact rational.
233if v == round(v) && abs(v) < 1e15
234 s = sprintf('%d', round(v));
236 s = sprintf('%.17g', v);