LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
getSymbolicDrift.m
1function [rhs, vars, sys] = getSymbolicDrift(self, options)
2% [RHS, VARS, SYS] = GETSYMBOLICDRIFT(OPTIONS)
3%
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.
6%
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.
11%
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.
19%
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
24%
25% Copyright (c) 2012-2026, Imperial College London
26% All rights reserved.
27
28if nargin < 2 || isempty(options)
29 options = self.getOptions();
30end
31sn = self.getStruct();
32sys = solver_fluid_symodes(sn, options);
33
34n = sys.nstates;
35vars = cell(1, n);
36for s = 1:n
37 vars{s} = sprintf('x%d', s);
38end
39
40% see _kb/06-solver-catalog.md for rationale
41eps0 = num2char(GlobalConstants.FineTol);
42
43switch sys.form
44 case 'W'
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.']);
49 end
50 rhs = wform_rhs(sys, vars, eps0);
51 case 'J'
52 rhs = jform_rhs(sys, vars, eps0);
53 otherwise
54 line_error(mfilename, sprintf('unsupported ODE form ''%s''', sys.form));
55end
56end
57
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.
62n = sys.nstates;
63theta = cell(1, n);
64for s = 1:n
65 if sys.isSource(s)
66 theta{s} = '0';
67 continue
68 end
69 i = sys.stateStation(s);
70 ni = stationSum(sys, i, vars, eps0);
71 S = sys.S(i);
72 p = sys.pstar(i);
73 if S <= 0 || p <= 0
74 theta{s} = vars{s};
75 else
76 theta{s} = sprintf('%s/(1 + (%s/%s)^%s)^(1/%s)', vars{s}, ni, ...
77 num2char(S), num2char(p), num2char(p));
78 end
79end
80
81rhs = cell(1, n);
82for s = 1:n
83 terms = {};
84 for t = 1:n
85 w = sys.W(t, s);
86 if w == 0 || strcmp(theta{t}, '0')
87 continue
88 end
89 terms{end+1} = sprintf('(%s)*(%s)', num2char(w), theta{t}); %#ok<AGROW>
90 end
91 if sys.Alambda(s) ~= 0
92 terms{end+1} = num2char(sys.Alambda(s)); %#ok<AGROW>
93 end
94 if isempty(terms)
95 rhs{s} = '0';
96 else
97 rhs{s} = strjoin(terms, ' + ');
98 end
99end
100end
101
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);
108for e = 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));
115 end
116 v = vars{sys.eventVar(e)};
117 fdata = sys.factorData{e};
118 switch ftype
119 case 'lin'
120 factor = v;
121 case 'ext1'
122 % 1 - sum of the class's phases 2..end at the source
123 if isempty(fdata.others)
124 factor = '1';
125 else
126 parts = cell(1, numel(fdata.others));
127 for k = 1:numel(fdata.others)
128 parts{k} = vars{fdata.others(k)};
129 end
130 factor = sprintf('(1 - (%s))', strjoin(parts, ' + '));
131 end
132 case 'dps'
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);
137 case 'fcfsws'
138 i = fdata.station;
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);
143 end
144 rate{e} = sprintf('(%s)*(%s)', num2char(sys.coeff(e)), factor);
145end
146
147rhs = cell(1, sys.nstates);
148for s = 1:sys.nstates
149 terms = {};
150 for e = 1:sys.nevents
151 j = sys.J(s, e);
152 if j == 0
153 continue
154 end
155 terms{end+1} = sprintf('(%s)*(%s)', num2char(j), rate{e}); %#ok<AGROW>
156 end
157 if isempty(terms)
158 rhs{s} = '0';
159 else
160 rhs{s} = strjoin(terms, ' + ');
161 end
162end
163end
164
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.
172a = num2char(alpha);
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);
175end
176
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));
181for k = 1:numel(idx)
182 parts{k} = vars{idx(k)};
183end
184if strcmp(offset, '0')
185 s = sprintf('(%s)', strjoin(parts, ' + '));
186else
187 s = sprintf('(%s + %s)', offset, strjoin(parts, ' + '));
188end
189end
190
191function s = weightedStationSum(sys, i, w, vars, offset)
192% sum_r w_ir * n_ir over the classes of station i (DPS denominator).
193parts = {};
194for k = 1:sys.nstates
195 if sys.stateStation(k) == i
196 wt = w(sys.stateClass(k));
197 if wt ~= 0
198 parts{end+1} = sprintf('(%s)*%s', num2char(wt), vars{k}); %#ok<AGROW>
199 end
200 end
201end
202s = joinSum(parts, offset);
203end
204
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).
208parts = {};
209for k = 1:sys.nstates
210 if sys.stateStation(k) == i
211 wt = sys.fcfsPhaseW(k);
212 if wt ~= 0
213 parts{end+1} = sprintf('(%s)*%s', num2char(wt), vars{k}); %#ok<AGROW>
214 end
215 end
216end
217s = joinSum(parts, offset);
218end
219
220function s = joinSum(parts, offset)
221% Sum of PARTS, with OFFSET added only when it is not the literal zero.
222if isempty(parts)
223 s = sprintf('(%s)', offset);
224elseif strcmp(offset, '0')
225 s = sprintf('(%s)', strjoin(parts, ' + '));
226else
227 s = sprintf('(%s + %s)', offset, strjoin(parts, ' + '));
228end
229end
230
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));
235else
236 s = sprintf('%.17g', v);
237end
238end