LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldes_busyperiod.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_LDES_LDES_BUSYPERIOD_H
6#define LINE_SOLVERS_LDES_LDES_BUSYPERIOD_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Busy period measurement for the native LDES engine.
12 *
13 * A busy period of ORDER n for a set of stations runs from the instant an
14 * arrival raises the jobs the set holds to n, up to the instant it falls back
15 * below n (Daduna, J. ACM 35(3), 1988). Order 1 is the ordinary busy period;
16 * higher orders measure how long the set stays at least n deep, which is what
17 * the analytical reference `pfqn_busyp` returns.
18 *
19 * ONE TARGET PER (set, class filter). Every station is a set of its own, every
20 * declared subnetwork is a set, and each is measured both aggregated over
21 * classes and per class, exactly as `initializeBusyPeriods` builds them.
22 *
23 * THE EXIT IS DEFERRED TO THE END OF THE INSTANT, and that is the whole
24 * difficulty. A job moving from one member of a target to another DROPS the
25 * count and RESTORES it at the same simulated instant; treating the drop as an
26 * exit ends a busy period that never ended and starts a new one, which roughly
27 * HALVES the reported mean on any multi-station target. So a drop only records
28 * a PENDING exit, an arrival at the same instant cancels it, and the pending
29 * exits are committed once the clock actually advances. A period of zero
30 * duration is an artifact of a simultaneous pair of events, not an
31 * observation, and is dropped.
32 */
33
34#include <cmath>
35#include <cstddef>
36#include <limits>
37#include <string>
38#include <vector>
39
40namespace line {
41namespace ldes {
42namespace engine {
43
44/** One measured target: a station set, optionally filtered to one class. */
45struct BpTarget {
46 std::vector<std::size_t> stations; ///< 0-based station indexes
47 int job_class = -1; ///< -1 aggregates every class
48 std::string name;
49};
50
51/**
52 * The busy-period tracker of one run.
53 *
54 * `orders` is the highest order measured; 0 disables the measurement and every
55 * entry point below returns immediately, so the engine pays nothing for it.
56 */
58public:
60
61 void init(const std::vector<BpTarget>& targets, int orders, std::size_t nstations) {
62 orders_ = orders;
63 if (orders_ <= 0) return;
64 targets_ = targets;
65 const std::size_t nt = targets_.size();
66 const std::size_t no = static_cast<std::size_t>(orders_);
67 jobs_.assign(nt, 0);
68 entry_.assign(nt, std::vector<double>(no, nan()));
69 pending_.assign(nt, std::vector<double>(no, nan()));
70 sum_.assign(nt, std::vector<double>(no, 0.0));
71 count_.assign(nt, std::vector<double>(no, 0.0));
72 by_station_.assign(nstations, std::vector<std::size_t>());
73 for (std::size_t ti = 0; ti < nt; ++ti)
74 for (std::size_t s : targets_[ti].stations)
75 if (s < nstations) by_station_[s].push_back(ti);
76 }
77
78 bool enabled() const { return orders_ > 0; }
79
80 /**
81 * Record a population change of `delta` (+1 or -1) for `cls` at `station`.
82 *
83 * `now` must be non-decreasing across calls: the first call at a strictly
84 * later instant is what commits the exits pending from the previous one.
85 */
86 void track(std::size_t station, std::size_t cls, int delta, double now) {
87 if (orders_ <= 0 || station >= by_station_.size()) return;
88 if (now > last_time_) {
89 commit();
90 last_time_ = now;
91 }
92 for (std::size_t ti : by_station_[station]) {
93 const BpTarget& t = targets_[ti];
94 if (t.job_class >= 0 && static_cast<std::size_t>(t.job_class) != cls) continue;
95 jobs_[ti] += delta;
96 // A rise to n opens the period of order n; a fall FROM n closes it,
97 // and after the decrement the count is n-1, hence the +1.
98 const int order = (delta > 0) ? jobs_[ti] : jobs_[ti] + 1;
99 if (order < 1 || order > orders_) continue;
100 const std::size_t oi = static_cast<std::size_t>(order - 1);
101 if (delta > 0) {
102 if (std::isnan(pending_[ti][oi]))
103 entry_[ti][oi] = now;
104 else
105 // The level came back within the same instant: the drop was
106 // a move inside the target and the period never ended.
107 pending_[ti][oi] = nan();
108 } else {
109 pending_[ti][oi] = now;
110 pending_list_.push_back(ti);
111 pending_list_.push_back(oi);
112 }
113 }
114 }
115
116 /** Close every exit no longer contradicted by a same-instant arrival. */
117 void commit() {
118 for (std::size_t p = 0; p + 1 < pending_list_.size(); p += 2) {
119 const std::size_t ti = pending_list_[p], oi = pending_list_[p + 1];
120 const double exit = pending_[ti][oi];
121 if (std::isnan(exit)) continue;
122 const double entry = entry_[ti][oi];
123 // A NaN entry belongs to a period straddling the warmup reset: its
124 // start is unknown, so the period is dropped rather than guessed.
125 if (!std::isnan(entry) && exit > entry) {
126 sum_[ti][oi] += exit - entry;
127 count_[ti][oi] += 1.0;
128 }
129 entry_[ti][oi] = nan();
130 pending_[ti][oi] = nan();
131 }
132 pending_list_.clear();
133 }
134
135 /**
136 * Discard everything observed during the warmup.
137 *
138 * A period IN PROGRESS at the reset has an unknown start, so its entry is
139 * invalidated and the period is dropped when it ends -- counting it from
140 * the reset instant would report a truncated period as a whole one and
141 * bias every mean downwards.
142 */
143 void reset() {
144 if (orders_ <= 0) return;
145 for (std::size_t ti = 0; ti < targets_.size(); ++ti)
146 for (std::size_t oi = 0; oi < static_cast<std::size_t>(orders_); ++oi) {
147 sum_[ti][oi] = 0.0;
148 count_[ti][oi] = 0.0;
149 entry_[ti][oi] = nan();
150 pending_[ti][oi] = nan();
151 }
152 pending_list_.clear();
153 }
154
155 const std::vector<BpTarget>& targets() const { return targets_; }
156 int orders() const { return orders_; }
157 /** Mean duration of order `oi`+1 at target `ti`; 0 when nothing was observed. */
158 double mean(std::size_t ti, std::size_t oi) const {
159 return (count_[ti][oi] > 0.0) ? sum_[ti][oi] / count_[ti][oi] : 0.0;
160 }
161 double count(std::size_t ti, std::size_t oi) const { return count_[ti][oi]; }
162
163private:
164 static double nan() { return std::numeric_limits<double>::quiet_NaN(); }
165
166 int orders_ = 0;
167 std::vector<BpTarget> targets_;
168 std::vector<int> jobs_;
169 std::vector<std::vector<double>> entry_, pending_, sum_, count_;
170 std::vector<std::vector<std::size_t>> by_station_;
171 std::vector<std::size_t> pending_list_;
172 double last_time_ = 0.0;
173};
174
175} // namespace engine
176} // namespace ldes
177} // namespace line
178
179#endif // LINE_SOLVERS_LDES_LDES_BUSYPERIOD_H
double mean(std::size_t ti, std::size_t oi) const
Mean duration of order oi+1 at target ti; 0 when nothing was observed.
const std::vector< BpTarget > & targets() const
void reset()
Discard everything observed during the warmup.
void commit()
Close every exit no longer contradicted by a same-instant arrival.
void init(const std::vector< BpTarget > &targets, int orders, std::size_t nstations)
double count(std::size_t ti, std::size_t oi) const
void track(std::size_t station, std::size_t cls, int delta, double now)
Record a population change of delta (+1 or -1) for cls at station.
One measured target: a station set, optionally filtered to one class.
int job_class
-1 aggregates every class
std::vector< std::size_t > stations
0-based station indexes