LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
NHPP.m
1classdef NHPP < ContinuousDistribution
2 % NHPP Non-homogeneous Poisson process with a piecewise-constant intensity.
3 %
4 % The intensity is a step function of the wall clock: segment i covers
5 % [breakpoints(i), breakpoints(i+1)) and carries rate rates(i), so
6 % breakpoints has one more entry than rates. With cyclic=true the schedule
7 % repeats, giving a cyclic Poisson process.
8 %
9 % Two horizon conventions:
10 % cyclic : the schedule repeats with period
11 % T = breakpoints(end) - breakpoints(1); the active segment
12 % at time t follows from mod(t - breakpoints(1), T).
13 % non-cyclic : the intensity is zero outside
14 % [breakpoints(1), breakpoints(end)), so the process emits
15 % nothing once the schedule is exhausted. A non-cyclic NHPP
16 % is therefore a transient construct: run to steady state it
17 % converges to the empty system, so callers should use a time
18 % span within the horizon.
19 %
20 % This is NOT a renewal process. Successive intervals are dependent, because
21 % the position within the schedule carries over from one event to the next.
22 % Accordingly the scalar summaries that presuppose an i.i.d. interval
23 % distribution -- getSCV, getSkewness, evalCDF, evalLST -- are undefined and
24 % return NaN rather than a representative exponential value, which would
25 % silently misreport the process as Poisson. The schedule is the
26 % parameterisation: read it with getRateSchedule. getMean is well defined and
27 % returns the arrival-stationary (Palm) mean interval 1/timeAverageRate.
28 %
29 % The process representation stores:
30 % process{1} : 1-by-(n+1) row vector of breakpoints
31 % process{2} : 1-by-n row vector of rates
32 % process{3} : logical, true if cyclic
33 %
34 % Solver support. The LDES simulation engine honours the exact schedule in
35 % both steady state (cyclic only) and transient analysis. SolverFLD honours
36 % it in getTranAvg, by injecting the intensity as a time-varying rate
37 % multiplier on the closing ODE; SolverFLD.getAvg uses the time-average
38 % rate, which is the steady state of a cyclic schedule. Every other solver
39 % rejects a model using it via the standard unsupported-feature check.
40 %
41 % Copyright (c) 2012-2026, Imperial College London
42 % All rights reserved.
43
44 properties
45 breakpoints; % 1-by-(n+1) segment boundaries, strictly increasing
46 rates; % 1-by-n non-negative rate on each segment
47 cyclic; % logical, whether the schedule repeats
48 process; % {breakpoints, rates, cyclic}, for serialization
49 sampleClock; % wall-clock position of the next sample (see sample)
50 end
51
52 methods
53 function self = NHPP(breakpoints, rates, cyclic)
54 % SELF = NHPP(BREAKPOINTS, RATES, CYCLIC)
55 self@ContinuousDistribution('NHPP', 0, [0, Inf]);
56 if nargin < 3 || isempty(cyclic)
57 cyclic = true;
58 end
59 if nargin < 2 || isempty(breakpoints) || isempty(rates) ...
60 || numel(breakpoints) ~= numel(rates) + 1
61 line_error(mfilename, 'NHPP: breakpoints must be non-empty with one more entry than rates');
62 end
63 breakpoints = breakpoints(:).';
64 rates = rates(:).';
65 if any(diff(breakpoints) <= 0)
66 line_error(mfilename, 'NHPP: breakpoints must be strictly increasing');
67 end
68 if any(rates < 0) || any(isinf(rates))
69 line_error(mfilename, 'NHPP: rates must be finite and non-negative');
70 end
71 if sum(rates .* diff(breakpoints)) <= 0
72 line_error(mfilename, 'NHPP: the schedule has zero total intensity, so no event can ever occur');
73 end
74 self.breakpoints = breakpoints;
75 self.rates = rates;
76 self.cyclic = logical(cyclic);
77 self.process = {self.breakpoints, self.rates, self.cyclic};
78 self.sampleClock = breakpoints(1);
79 self.mean = 1.0 / self.getTimeAverageRate();
80 self.immediate = false;
81 end
82
83 function b = getBreakpoints(self)
84 b = self.breakpoints;
85 end
86
87 function r = getRates(self)
88 r = self.rates;
89 end
90
91 function c = isCyclic(self)
92 c = self.cyclic;
93 end
94
95 function n = getNumSegments(self)
96 n = numel(self.rates);
97 end
98
99 function T = getPeriod(self)
100 % T = GETPERIOD() Horizon length, which is the period when cyclic.
101 T = self.breakpoints(end) - self.breakpoints(1);
102 end
103
104 function rate = getTimeAverageRate(self)
105 % RATE = GETTIMEAVERAGERATE()
106 % sum(rates.*widths)/sum(widths). For a cyclic schedule this is the
107 % long-run arrival rate; for a non-cyclic one it averages over the
108 % active horizon only, the intensity being zero afterwards.
109 rate = sum(self.rates .* diff(self.breakpoints)) / self.getPeriod();
110 end
111
112 function r = getRateAt(self, t)
113 % R = GETRATEAT(T) Rate in force at T; zero past a non-cyclic horizon.
114 T = self.getPeriod();
115 offset = t - self.breakpoints(1);
116 if self.cyclic
117 offset = mod(offset, T);
118 elseif offset < 0 || offset >= T
119 r = 0.0;
120 return
121 end
122 pos = self.breakpoints(1) + offset;
123 idx = find(pos < self.breakpoints(2:end), 1, 'first');
124 if isempty(idx)
125 idx = numel(self.rates);
126 end
127 r = self.rates(idx);
128 end
129
130 function sched = getRateSchedule(self)
131 % SCHED = GETRATESCHEDULE()
132 % The parameterisation of the process; the scalar interval summaries
133 % are not. Model compilation recognises a schedule-bearing process by
134 % this method rather than by class name.
135 sched = struct('breakpoints', self.breakpoints, ...
136 'rates', self.rates, 'cyclic', self.cyclic);
137 end
138
139 function mean = getMean(self)
140 % MEAN = GETMEAN() Arrival-stationary (Palm) mean interval.
141 mean = 1.0 / self.getTimeAverageRate();
142 end
143
144 function scv = getSCV(self)
145 % SCV = GETSCV()
146 % NaN: an NHPP is not a renewal process, so there is no i.i.d.
147 % interval distribution for an SCV to summarise. Returning a
148 % representative value here would report a time-varying process as an
149 % exponential one to every consumer of sn.scv.
150 scv = NaN;
151 end
152
153 function skew = getSkewness(self)
154 % SKEW = GETSKEWNESS() NaN; see getSCV.
155 skew = NaN;
156 end
157
158 function F = evalCDF(self, t)
159 % F = EVALCDF(T) NaN; see getSCV.
160 F = NaN(size(t));
161 end
162
163 function L = evalLST(self, s)
164 % L = EVALLST(S) NaN; see getSCV.
165 L = NaN(size(s));
166 end
167
168 function proc = getProcess(self)
169 proc = self.process;
170 end
171
172 function resetSampleClock(self)
173 % RESETSAMPLECLOCK() Restart the sample path at the schedule start.
174 self.sampleClock = self.breakpoints(1);
175 end
176
177 function X = sample(self, n)
178 % X = SAMPLE(N)
179 % Draws N successive interarrival times along ONE sample path. The
180 % intensity depends on absolute time, so this advances an internal
181 % clock across calls: consecutive samples form a realisation of the
182 % process starting at breakpoints(1), not independent draws from a
183 % marginal. Use resetSampleClock to restart. A non-cyclic schedule
184 % that runs out returns 0 for every remaining sample, the intensity
185 % there being zero.
186 if nargin < 2
187 n = 1;
188 end
189 X = zeros(n, 1);
190 for i = 1:n
191 residual = -log(1 - rand());
192 interval = self.nextInterval(self.sampleClock, residual);
193 X(i) = interval;
194 if interval <= 0
195 break % horizon exhausted: no further event can occur
196 end
197 self.sampleClock = self.sampleClock + interval;
198 end
199 end
200
201 function x = nextInterval(self, from, residual)
202 % X = NEXTINTERVAL(FROM, RESIDUAL)
203 % Solves int_{from}^{from+x} lambda(u) du = RESIDUAL for X, walking
204 % the schedule forward and consuming the budget segment by segment.
205 % Returns 0 when a non-cyclic horizon is exhausted first, which
206 % callers read as "no further event".
207 %
208 % Exact for an NHPP: conditional on no event since the last one, the
209 % residual is governed by the intensity from the current instant
210 % onward, so a holding time drawn under a rate that has since changed
211 % is not a sample from this process.
212 T = self.getPeriod();
213 offset = from - self.breakpoints(1);
214 if self.cyclic
215 offset = mod(offset, T);
216 elseif offset >= T
217 x = 0.0;
218 return
219 elseif offset < 0
220 offset = 0.0;
221 end
222 pos = self.breakpoints(1) + offset;
223 idx = 1;
224 while idx < numel(self.rates) && pos >= self.breakpoints(idx+1)
225 idx = idx + 1;
226 end
227 elapsed = 0.0;
228 while true
229 remainingInSegment = self.breakpoints(idx+1) - pos;
230 massInSegment = self.rates(idx) * remainingInSegment;
231 % The rate guard also keeps a zero-rate segment from dividing 0/0
232 % on the measure-zero draw residual == 0.
233 if self.rates(idx) > 0 && massInSegment >= residual
234 x = elapsed + residual / self.rates(idx);
235 return
236 end
237 residual = residual - massInSegment;
238 elapsed = elapsed + remainingInSegment;
239 idx = idx + 1;
240 if idx > numel(self.rates)
241 if ~self.cyclic
242 x = 0.0;
243 return
244 end
245 idx = 1;
246 end
247 pos = self.breakpoints(idx);
248 end
249 end
250 end
251end
Definition Station.m:245