LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ssa_event_cache.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_SSA_SSA_EVENT_CACHE_H
6#define LINE_SOLVERS_SSA_SSA_EVENT_CACHE_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `EventCache.m` and of the lookup that `State.afterEvent` performs
12 * against it (afterEvent.m lines 24-44 and its write-back sites).
13 *
14 * WHAT IS BEING MEMOIZED, AND WHY IT IS SOUND. `after_event` is a pure function
15 * of `(sn, ind, inspace, event, cls, no_promote, aux_rate)`: it reads no
16 * mutable state, draws no random number and touches nothing outside its
17 * arguments. So its value may be stored against those arguments and returned
18 * again, and a hit is EXACTLY what recomputation would produce. That is the
19 * whole correctness condition, and it is the one the tests assert directly,
20 * state by state, rather than inferring it from the metrics.
21 *
22 * THE KEY IS THE WHOLE ARGUMENT LIST. The reference keys on
23 * `mat2str([ind, event, class, noPromote, inspace])`, and each of those five
24 * pieces earns its place: `noPromote` distinguishes the departure half of an
25 * immediate-feedback self-loop (which leaves the vacated server held) from an
26 * ordinary departure at the same station in the same state, and dropping it
27 * would return one where the other was asked for. This port keys on the same
28 * five plus `aux_rate`, which the C++ handler takes and MATLAB does not: it is
29 * the rate carried into the RENEGE, RETRY, FAILURE and REPAIR branches, so two
30 * calls that differ only in it have different answers.
31 *
32 * A HIT AND A MISS ARE INDISTINGUISHABLE HERE, WHICH IS STRONGER THAN THE
33 * REFERENCE. MATLAB stores the full enumeration and then, on a hit, SAMPLES one
34 * successor row from it, spending a `rand`. This port's `after_event` is the
35 * enumeration-mode handler in every case and the sampling happens downstream in
36 * the engine, so enabling the cache changes neither the value nor the random
37 * stream. The seed-fixed sample path is therefore identical with and without
38 * caching, which is what lets a test compare the two runs bit for bit.
39 *
40 * THE CACHE IS BOUND TO ONE `sn`. The reference's `create` takes `sn` and
41 * ignores it (its loops are commented out). Here it is retained and checked:
42 * the memoized value is computed from the rates, capacities and routing of one
43 * network struct, so serving it to a query about a different struct would
44 * return another model's successors. The binding is a pointer identity test
45 * because the engines hold `sn` by const reference for their whole lifetime.
46 *
47 * A DISABLED CACHE IS NOT AN ABSENT ONE. `EventCache.create(false, sn)` returns
48 * `[]` and `afterEvent` then takes the uncached path. Here a disabled cache is
49 * an object that computes on every call and stores nothing, so the caller has
50 * one code path rather than two and cannot accidentally diverge between them.
51 */
52
53#include <cstddef>
54#include <map>
55#include <vector>
56
60#include "line/num/number.h"
61#include "line/util/error.h"
62
63namespace line {
64namespace ssa {
65
66/**
67 * The memoization key: the argument list of `after_event`, in fields.
68 *
69 * The pieces are kept SEPARATE rather than flattened into one vector because a
70 * flatten lets a wide state row of one node impersonate a narrow row of another
71 * with a different index prefix; separate fields cannot collide by
72 * construction, so no separator sentinel is needed.
73 */
75 std::size_t ind = 0; ///< 1-based node index
76 int event = 0; ///< `EventType`, as its underlying value
77 std::size_t cls = 0; ///< 1-based class index
78 bool no_promote = false;
79 double aux_rate = 0.0;
80 std::vector<double> inspace; ///< the node's local state row
81
82 bool operator<(const SsaEventKey& o) const {
83 if (ind != o.ind) return ind < o.ind;
84 if (event != o.event) return event < o.event;
85 if (cls != o.cls) return cls < o.cls;
86 if (no_promote != o.no_promote) return !no_promote;
87 if (aux_rate != o.aux_rate) return aux_rate < o.aux_rate;
88 return inspace < o.inspace;
89 }
90};
91
92/** `EventCache`: the per-state enabled-event memo of the serial SSA engine. */
93template <class T>
95public:
96 /**
97 * `EventCache.create(enabled, sn)`.
98 *
99 * The struct is held by pointer, so the cache must not outlive it. Every
100 * caller in this port constructs the cache inside the scope that already
101 * holds `sn` by const reference for the run.
102 */
104 return SsaEventCache(enabled, &sn);
105 }
106
107 SsaEventCache() : enabled_(false), sn_(0) {}
108 SsaEventCache(bool enabled, const qn::NetworkStruct<T>* sn) : enabled_(enabled), sn_(sn) {}
109
110 bool enabled() const { return enabled_; }
111
112 /**
113 * `State.afterEvent` with the lookup in front of it.
114 *
115 * `sn` is passed rather than taken from the binding so the signature is the
116 * free function's and a caller can switch between them by changing one
117 * token; the binding is only there to catch the mistake of reusing a cache
118 * across structs.
119 */
121 const std::vector<T>& inspace, lang::EventType event,
122 std::size_t cls, bool no_promote = false,
123 const T& aux_rate = num_traits<T>::from_int(0)) {
124 if (!enabled_)
125 return qn::after_event(sn, ind, inspace, event, cls, no_promote, aux_rate);
126 if (sn_ != 0 && sn_ != &sn)
127 throw InputError(
128 "SsaEventCache: queried with a different NetworkStruct from the one it was "
129 "created against. The memoized successors are a function of that struct's rates, "
130 "capacities and routing, so serving them here would answer about another model");
131
132 SsaEventKey key;
133 key.ind = ind;
134 key.event = static_cast<int>(event);
135 key.cls = cls;
136 key.no_promote = no_promote;
137 key.aux_rate = num_traits<T>::to_double(aux_rate);
138 key.inspace.resize(inspace.size());
139 for (std::size_t j = 0; j < inspace.size(); ++j)
140 key.inspace[j] = num_traits<T>::to_double(inspace[j]);
141
142 const typename std::map<SsaEventKey, qn::EventOutcome<T> >::const_iterator it =
143 memo_.find(key);
144 if (it != memo_.end()) {
145 ++hits_;
146 return it->second;
147 }
148 ++misses_;
149 const qn::EventOutcome<T> out =
150 qn::after_event(sn, ind, inspace, event, cls, no_promote, aux_rate);
151 memo_[key] = out;
152 return out;
153 }
154
155 /**
156 * Hits and misses, so a caller can report the memo's effect.
157 *
158 * They are counters and not a hit RATIO because the ratio alone hides the
159 * denominator, and a 100% hit rate over three lookups says nothing.
160 */
161 std::size_t hits() const { return hits_; }
162 std::size_t misses() const { return misses_; }
163 std::size_t size() const { return memo_.size(); }
164
165 void clear() {
166 memo_.clear();
167 hits_ = 0;
168 misses_ = 0;
169 }
170
171private:
172 bool enabled_;
173 const qn::NetworkStruct<T>* sn_;
174 std::map<SsaEventKey, qn::EventOutcome<T> > memo_;
175 std::size_t hits_ = 0, misses_ = 0;
176};
177
178} // namespace ssa
179} // namespace line
180
181#endif // LINE_SOLVERS_SSA_SSA_EVENT_CACHE_H
InputError(const std::string &what)
Definition error.h:39
A network plus its refreshed NetworkStruct.
std::size_t size() const
std::size_t hits() const
Hits and misses, so a caller can report the memo's effect.
qn::EventOutcome< T > after_event(const qn::NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, lang::EventType event, std::size_t cls, bool no_promote=false, const T &aux_rate=num_traits< T >::from_int(0))
State.afterEvent with the lookup in front of it.
static SsaEventCache create(bool enabled, const qn::NetworkStruct< T > &sn)
EventCache.create(enabled, sn).
std::size_t misses() const
SsaEventCache(bool enabled, const qn::NetworkStruct< T > *sn)
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
EventType
The events a state can undergo, with the values of MATLAB EventType.
Definition lang_types.h:111
EventOutcome< T > after_event(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< T > &inspace, EventType event, std::size_t cls, bool no_promote=false, const T &aux_rate=num_traits< T >::from_int(0))
Port of State.afterEvent: the successors of one event at one NODE.
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
Port of the event half of MATLAB's +State package: the successor states an event produces at one node...
What one event produces at one node: the successor rows, their rates and their probabilities,...
The memoization key: the argument list of after_event, in fields.
int event
EventType, as its underlying value
std::vector< double > inspace
the node's local state row
std::size_t cls
1-based class index
bool operator<(const SsaEventKey &o) const
std::size_t ind
1-based node index