1classdef NHPP < ContinuousDistribution
2 % NHPP Non-homogeneous Poisson process with a piecewise-constant intensity.
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.
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.
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.
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
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.
41 % Copyright (c) 2012-2026, Imperial College London
42 % All rights reserved.
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)
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)
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');
63 breakpoints = breakpoints(:).
';
65 if any(diff(breakpoints) <= 0)
66 line_error(mfilename, 'NHPP: breakpoints must be strictly increasing');
68 if any(rates < 0) || any(isinf(rates))
69 line_error(mfilename, 'NHPP: rates must be finite and non-negative');
71 if sum(rates .* diff(breakpoints)) <= 0
72 line_error(mfilename, 'NHPP:
the schedule has zero total intensity, so no event can ever occur');
74 self.breakpoints = breakpoints;
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;
83 function b = getBreakpoints(self)
87 function r = getRates(self)
91 function c = isCyclic(self)
95 function n = getNumSegments(self)
96 n = numel(self.rates);
99 function T = getPeriod(self)
100 % T = GETPERIOD() Horizon length, which
is the period when cyclic.
101 T = self.breakpoints(end) - self.breakpoints(1);
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();
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);
117 offset = mod(offset, T);
118 elseif offset < 0 || offset >= T
122 pos = self.breakpoints(1) + offset;
123 idx = find(pos < self.breakpoints(2:end), 1, 'first');
125 idx = numel(self.rates);
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);
139 function mean = getMean(self)
140 % MEAN = GETMEAN() Arrival-stationary (Palm) mean interval.
141 mean = 1.0 / self.getTimeAverageRate();
144 function scv = getSCV(self)
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.
153 function skew = getSkewness(self)
154 % SKEW = GETSKEWNESS() NaN; see getSCV.
158 function F = evalCDF(self, t)
159 % F = EVALCDF(T) NaN; see getSCV.
163 function L = evalLST(self, s)
164 % L = EVALLST(S) NaN; see getSCV.
168 function proc = getProcess(self)
172 function resetSampleClock(self)
173 % RESETSAMPLECLOCK() Restart
the sample path at
the schedule start.
174 self.sampleClock = self.breakpoints(1);
177 function X = sample(self, 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
191 residual = -log(1 - rand());
192 interval = self.nextInterval(self.sampleClock, residual);
195 break % horizon exhausted: no further event can occur
197 self.sampleClock = self.sampleClock + interval;
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".
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);
215 offset = mod(offset, T);
222 pos = self.breakpoints(1) + offset;
224 while idx < numel(self.rates) && pos >= self.breakpoints(idx+1)
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);
237 residual = residual - massInSegment;
238 elapsed = elapsed + remainingInSegment;
240 if idx > numel(self.rates)
247 pos = self.breakpoints(idx);