This chapter adds weights to the transition systems of chapter 9. Deck 07, "Stochastic modelling: uncertainty, probability and frequencies", opens by recalling the two sources of uncertainty identified at the end of the previous lecture — openness (new users/services/devices keep arriving; heterogeneity; human behaviour not pre-coded) and unpredictability (failures become the norm; delays and losses; only statistical characterisation; effects only estimated) — and then delivers the diagnosis of the day:
"On the non-determinism modelling mechanism: captures variability of 'outcomes' · does not 'weight' them · does not distinguish what is normal and what exceptional, and neglects performance."
A transition system knows which outcomes are possible; it cannot answer the two questions that matter at scale:
The deck's motivations for quantitative modelling are worth reading as a list of exam-project ideas: specifying that some (even unlikely) events may occur, such as a node failing, but this is an exception case; that among the nodes sending messages one is faster, hence it is more probable that a given message comes from it; a timing model for the events captured/produced by a sensor; a nature-inspired approach for solving some computational problem — "in physics, chemistry, cellular biology, ant-colonies (in nature every mechanism is noisy, hence probabilistic, and this is key)"; the probabilistic "performance" of a system ("99% of times it completes its job in 1 hour"); reactiveness/resiliency expressed as the average time needed to recover at least 90% from a fault; a plan guaranteeing achievement of a goal with 60% probability.
The method, per the deck: "Use 'labels' to represent probability, frequency, decisions" — derive enhanced models such as stochastic Petri Nets. The roadmap of the chapter is the evolution of the meta-meta-model:
In the vocabulary of the modelling stack of chapter 9, section 8: the meta-meta-model evolves from RS to CTMC, and the meta-model evolves from Petri Nets to Stochastic Petri Nets. Everything built in chapter 9 — System, MSet, toSystem — survives the upgrade almost unchanged.
The deck is explicit: probabilities typically come from information on the past (the ratio between how many times a transition occurred and how many times it could have occurred), hence "probabilities typically hide some unknown detail, and are hence a (powerful) abstraction tool to deal with uncertainty". Non-determinism says "there are many options"; probability adds "and here is how frequently each one is expected". The whole chapter is the machinery for making that sentence executable.
A Discrete-Time Markov Chain is "an extension of RS": the same couple ⟨S, →⟩, but the relation now carries a probability:
It is a couple ⟨S, →⟩ where → ⊆ S × [0, 1] × S — that is, a directed graph where edges are labelled with probabilities; write
S -p→ S′when ⟨S, p, S′⟩ ∈ →.Sanity condition 1: ∀S ∈ S, either S is a deadlock (S ↛), or the outgoing arcs have total probability 1.
Sanity condition 2: there is at most one arc from S to S′.
The two sanity conditions say: from every non-deadlock state, the outgoing probabilities are a genuine distribution (they sum to 1), and the graph is simple (no duplicate edges). The deck immediately relaxes the first condition with likelihood factors (LF):
10, 10, 30 are equivalent to probabilities 0.2, 0.2, 0.6;Finally, a pure DTMC is a finite set S and a transition probability function T : S × S ↦ [0, 1] — "namely, a square matrix of non-negative reals; probability 0 means there can't be any transition". The matrix view is the one PRISM uses in chapter 11.
10, 10, 30 → 0.2, 0.2, 0.6. The pure DTMC is just the matrix T.Two questions accompany every probabilistic model: where do the numbers come from, and how do we draw from them when we animate or simulate?
Where do probabilities come from? "Typically, probabilities are obtained by information on the past, as the ratio between how many times a transition occurred, over the times it could have occurred." This makes probability "a (powerful) abstraction tool to deal with uncertainty": the label hides the unknown detail behind a frequency. And because it is a refinement of non-determinism, "it tells us how frequently a certain 'event' is expected to happen in the future".
How do we draw results fairly? The standard technique is inverse transform sampling: build the cumulative distribution function (CDF) of the distribution — for a positive-real-valued random variable X, FX(x) = Pr(X ≤ x), with FX : ℝ⁺₀ ↦ [0, 1], f(0) = 0, f(∞) = 1 — then compute FX⁻¹(n) where n is a uniform random number in [0, 1]. For a finite set of weighted outcomes, the CDF is the cumulative list of weights, and the draw is the first outcome whose cumulative weight exceeds the random number — exactly the algorithm the next section implements.
n is projected onto the inverse CDF. On the right, the finite case used for DTMCs: cumulative weights and the rule i = min{j : Σ_{k≤j} pₖ ≥ n}. The same machinery returns in section 6 for drawing the duration of CTMC steps.The deck gives the essential algorithm for interpreting a DTMC — yielding a path and its overall probability:
s equal to the initial state s0, and current probability p = 1;[(p1, s1), ..., (pn, sn)];n in [0, 1];i = min{j : Σ_{k=1..j} pₖ ≥ n};s = sᵢ with probability p = pᵢ;The sample trace printed in the deck alternates between states s1, s2, s3 with probabilities like 0.6, 0.2 and 1.0 — a run of exactly this interpreter. The toolkit that implements it is Stochastics, a small object with three operations: cumulative (turn weighted choices into the cumulative list), draw (select one outcome fairly), and statistics (repeat the draw many times and tally):
object Stochastics:
given Random = new Random()
// (p1, a1),...,(pn, an) --> (p1, a1), (p1+p2, a2), ..., (p1+..+pn, an)
def cumulative[A](l: List[(Double, A)]): List[(Double, A)] =
l.tail.scanLeft(l.head):
case ((r, _), (r2, a2)) => (r + r2, a2)
// (p1, a1),...,(pn, an) --> ai, selected randomly and fairly
def draw[A](cumulativeList: List[(Double, A)])(using rnd: Random): A =
val rndVal = rnd.nextDouble() * cumulativeList.last._1
cumulativeList.collectFirst:
case (r, a) if r >= rndVal => a
.get
// (p1, a1),...,(pn, an) --> { a1 -> P1%, ..., an -> Pn% }
def statistics[A](choices: Set[(Double, A)], size: Int)
(using rnd: Random): Map[A, Int] =
(1 to size).map(_ => draw(cumulative(choices.toList)))
.groupBy(identity).view.mapValues(_.size).toMap
The unit test in the deck pins the behaviour with a tolerance — and carries an honest editorial note: "to be improved, e.g. with random monad!" (the monad machinery of chapter 7 reappears here as a testing concern). With choices 1.0 → "a", 2.0 → "b", 3.0 → "c", the cumulative list is List((1.0, "a"), (3.0, "b"), (6.0, "c")), and over 10 000 draws the counts must land within ±500 of the expected 1666 / 3333 / 5000:
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers.*
import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper
class StochasticsSpec extends AnyFunSuite:
import Stochastics.given
val choices = Set(1.0 -> "a", 2.0 -> "b", 3.0 -> "c")
test("Choices should correctly give cumulative list"):
Stochastics.cumulative(choices.toList) shouldBe
List((1.0, "a"), (3.0, "b"), (6.0, "c"))
test("Choices should correctly draw"):
val map = Stochastics.statistics(choices, 10000)
map("a") shouldBe 1666 +- 500
map("b") shouldBe 3333 +- 500
map("c") shouldBe 5000 +- 500
Be able to trace draw by hand: the random value is scaled by the total weight (cumulativeList.last._1), and collectFirst returns the first outcome whose cumulative weight reaches it — the discrete version of FX⁻¹(n). And be able to discuss the testing problem: with randomness, unit tests must either use tolerances (as the deck does), a seeded Random, or a random monad that makes draws deterministic values — the RANDOM-UNIT-TESTER lab task of section 10.
Probabilities answer "how likely"; the next question is "when". The deck starts from the observation that "many natural/computational phenomena are timed": it is key to consider when something happens, continuously — "time is hence a mechanism to consider when our design has to be validated against timing aspects, and when we are generally concerned about the actual reactivity of a system in providing a certain service".
And which timing model for transitions? One extreme: label a transition with a positive real number saying exactly how much time the transition takes — "example incarnations based on this idea include Timed Petri Nets", but "this model is considered too synchronous for real distributed systems". The middle ground is a semi-synchronous timing model: the positive real attached to a transition is its average expected time — "which can actually be lower or higher in each case, depending on probability" — and "sometimes, average frequency is instead used, also called rate". The model is captured by a cumulative distribution function describing the probability that a transition fires after a given amount of time elapsed; several such functions could be used:
CTMC (Continuous-Time Markov Chains) — as an extension of TS. It is a couple ⟨S, →⟩ where → ⊆ S × ℝ⁺ × S: a directed graph where each edge is labelled with a (Markovian) rate
r; transitions are probabilistic and timed, using as cumulative distribution function the exponential one1 − e^(−t·r).
The exponential distribution is the deck's choice, and the reasons are given in full. First, empirical: many natural/artificial systems follow it — "radioactive particle decays, arrival of the next email (in a small period), mutations on a DNA strand, roadkill on a given street" — and "it is often used when not much information is known about variability". Second, and deeper: it is the distribution of memoryless processes:
Formally, let X be the time at which an event occurs: memoryless means
P(X > s + t | X > t) = P(X > s).Then
P(X > s + t) = P(X > s) · P(X > t), andP(X > t)decreases in t. The only continuous function satisfying this isP(X > t) = e^(−r·t), where1/ris the average value — or vice versaP(X ≤ t) = 1 − e^(−r·t). "In fact, note that this is a 'self-similar' function..."
If the event has not happened yet, nothing new is known about when it will happen in the future — the process has no memory, and the exponential is the only continuous distribution with that property. The rate r is the average frequency, so the average waiting time is 1/r.
1 − e^(−t·r) and average 1/r; the distribution is memoryless, and the exponential is the only continuous distribution with that property. The stochastic channel of the deck is the running example: SEND fires 400 000 times per second in total, so retries, successes and failures happen almost immediately, while IDLE → SEND at rate 1 makes the idle wait the slow part.In a CTMC several transitions are usually allowed from the same state. How do we draw the choice, and how do we state how much time passed? "Some simple math" answers both:
Assume in a state S several transitions are allowed:
S -r1→ S1, S -r2→ S2, ..., S -rn→ Sn.1. the probability of transiting to
Siisri/R, whereR = r1 + r2 + ... + rn;2. S will transit to a new state at Markovian rate
R.⇒ see "Distribution of the minimum of exponential random variables":
P(X > min(t1, ..., tn)) = 1 − P(X < min(t1, ..., tn)) = 1 − e^(−r1·t) · ... · e^(−rn·t) = 1 − e^(−(r1+...+rn)·t).
The intuition: each competing transition has its own exponential clock; the one that rings first wins. The minimum of independent exponentials with rates r1..rn is exponential with rate R = Σri, and the probability that clock i is the first is ri/R — proportional to its rate. Two consequences worth stating: "the probability part of CTMC is the same as a DTMC (with normalisation)" — the CTMC's embedded chain is a DTMC — and "DTMC equates a CTMC if we forget about its duration aspects".
Simulation of a CTMC is done by Gillespie's algorithm. One simulation step:
(S1, r1), ..., (Sn, rn);R be the sum of rates Σ ri;Si by probability ri/R, using a random number τ1;Δt by exponential distribution with rate R, using a random number τ2: Δt = (1/R) · log(1/τ2).Simulation results have the form [S0s, t0s], [S1s, t1s], ..., [Sns, tns] — a trace of states with their cumulative times. The deck notes: "as expected, the same as DTMC when duration is not considered", and "drawing duration is essentially computing the inverse of CDF on a random number".
ri/R, and the duration is drawn as exponential with rate R — Δt = (1/R)·log(1/τ2) is precisely the inverse-CDF sampling of section 3 applied to the exponential.The meta-meta-model is a trait with one operation, mirroring System of chapter 9, section 4:
trait CTMC[S]:
import CTMC.Action
def transitions(a: S): Set[Action[S]] // rate + state
object CTMC:
case class Action[S](rate: Double, state: S)
extension [S](rate: Double)
def -->(state: S) = Action(rate, state)
case class Transition[S](state: S, action: Action[S])
def ofFunction[S](f: PartialFunction[S, Set[Action[S]]]): CTMC[S] =
s => f.applyOrElse(s, x => Set[Action[S]]())
def ofRelation[S](rel: Set[Transition[S]]): CTMC[S] =
ofFunction(s => rel.filter(_.state == s).map(_.action))
def ofTransitions[S](rel: Transition[S]*): CTMC[S] = ofRelation(rel.toSet)
The stochastic channel of section 5 is one expression — note the rates: IDLE → SEND is slow (rate 1), everything inside SEND is fast, and DONE → DONE at rate 1 makes success sticky:
object StochasticChannel:
enum State:
case IDLE, SEND, DONE, FAIL;
export State.*
export u07.modelling.CTMCSimulation.*
def stocChannel: CTMC[State] = CTMC.ofTransitions(
Transition(IDLE, 1.0 --> SEND),
Transition(SEND, 100000.0 --> SEND),
Transition(SEND, 200000.0 --> DONE),
Transition(SEND, 100000.0 --> FAIL),
Transition(FAIL, 100000.0 --> IDLE),
Transition(DONE, 1.0 --> DONE)
)
Simulation is a lazy stream of events — Trace = LazyList[Event] — built with LazyList.iterate. A deadlock state repeats itself at the same time; otherwise the next state is drawn from the normalised rates and the time advances by the exponential draw:
import java.util.Random
import u07.utils.Stochastics
object CTMCSimulation:
case class Event[A](time: Double, state: A)
type Trace[A] = LazyList[Event[A]]
export CTMC.*
extension [S](self: CTMC[S])
def newSimulationTrace(s0: S, rnd: Random): Trace[S] =
LazyList.iterate(Event(0.0, s0)):
case Event(t, s) =>
if self.transitions(s).isEmpty
then Event(t, s)
else
val choices = self.transitions(s) map (t => (t.rate, t.state))
val next = Stochastics.cumulative(choices.toList)
val sumR = next.last._1
val choice = Stochastics.draw(next)(using rnd)
Event(t + Math.log(1 / rnd.nextDouble()) / sumR, choice)
The deck prints a sample run of the DTMC interpreter of section 4 as successive [probability, state] pairs — lines such as [5.9999999999999998e-1, s1] (probability 0.6 towards s1), [2.0000000000000001e-1, s2] (0.2 towards s2), and occasional [1.0e+1, s1] / [1.0, s3] — the drawing of a model whose labels mix probabilities and likelihood factors. The exact model is the slide's example; what matters is the shape: each line is one draw of the next state weighted by its label. For the stochastic channel, the widget below lets you draw such steps yourself, with the actual rates of the lecture.
The Gillespie step uses both sampling tools of section 3: draw selects the transition (discrete inverse CDF over the cumulative rates), and Math.log(1/rnd.nextDouble())/sumR draws the duration (continuous inverse CDF of the exponential). Everything a simulation produces — traces, averages, percentages — is statistics over such draws, which is why the same Stochastics object serves DTMC interpretation, CTMC simulation, and later the approximate model-checking of chapter 11, section 8.
The meta-model upgrade is the promised one: PN + CTMC. "Each Petri transition is associated with a rate — such a rate could be a formula of the number of tokens in each incoming place. When the transition can fire it competes with all other pending ones. Probabilistically, the fastest one wins, and a duration is applied."
The structural change to Trn is minimal: cond, eff and inh stay, and a rate function is added — MSet[P] => Double, so the rate may depend on the current marking. The compilation step is the CTMC analogue of toSystem:
object SPN:
// pre-conditions, rate, effects, inhibition
case class Trn[P](
cond: MSet[P],
rate: MSet[P] => Double,
eff: MSet[P],
inh: MSet[P])
type SPN[P] = Set[Trn[P]]
def toCTMC[P](spn: SPN[P]): CTMC[MSet[P]] =
m =>
for
Trn(cond, rate, eff, inh) <- spn
if m disjoined inh
r = rate(m)
out <- m extract cond
yield Action(r, out union eff)
def apply[P](transitions: Trn[P]*): SPN[P] = transitions.toSet
Compare with PetriNet.toSystem of chapter 9, section 7: the only differences are the rate function (computed on the current marking) and the target type (CTMC[MSet[P]] instead of System[MSet[P]]). The enablement machinery — inhibition check, extract, union eff — is identical. The meta-modelling promise of chapter 9 pays off: the DSL changed by three lines.
The deck's canonical application is producers and consumers: two producers send data items at rates rp1 and rp2, one consumer processes items at rate rc:
rp1 rp2 rc
M ────────→ d | M M ────────→ d | M M | d ─────→ M
And the stochastic version of mutual exclusion shows rates as marking-dependent formulas: the first transition has fixed rate 1.0, the second has rate m(T) — "the more processes are trying, the faster one of them gets in" — and the third has fixed rate 2.0:
object StochasticMutualExclusion extends App:
// Specification of my data-type for states
enum Place:
case N, T, C
export Place.*
export u07.modelling.CTMCSimulation.*
export u07.modelling.SPN.*
val spn = SPN[Place](
Trn(MSet(N), m => 1.0, MSet(T), MSet()),
Trn(MSet(T), m => m(T), MSet(C), MSet(C)),
Trn(MSet(C), m => 2.0, MSet(), MSet()))
println:
toCTMC(spn).newSimulationTrace(MSet(N, N, N, N), new Random)
.take(20)
.toList.mkString("\n")
The stochastic readers/writers of the deck give the full rate table (to be reused in PRISM in chapter 11, section 6): t1: 1.0 (a fixed rate of actual arrival of requests), t2: 200000 (almost immediate, 66% of readers), t3: 100000 (almost immediate, 33% of writers), t4: 100000 and t5: 100000 (immediate), t6: 0.1·p6 (average "rate" of reading is 0.1, scaled by the number of readers), t7: 0.2 (average "rate" of writing is 0.2).
m(T)) are the deck's examples. toCTMC compiles the SPN into a plain CTMC[MSet[P]], so the Gillespie simulator of section 6 runs any net unchanged.Be able to derive the competing-race semantics by hand on the stochastic channel: from SEND the rates are 100000, 200000, 100000, so R = 400000 and the outcome probabilities are 0.25 (retry), 0.5 (DONE), 0.25 (FAIL) — the channel is twice as likely to succeed as to fail or retry, and the waiting time at SEND averages 1/400000 seconds. From IDLE, the wait to SEND averages 1 second, and a failed attempt adds about 1/100000 + 1 seconds; solving the expectation gives about 1.5 seconds from IDLE to DONE — the number the SIMULATOR lab task should reproduce across many runs.
The deck makes a striking observation: "This is precisely the chemical settings!!!" The stochastic mutual exclusion of section 7 is structurally the sodium-chloride reaction:
Ionization law: Na + Cl ──rio──→ Na+ + Cl−
Deionization law: Na+ + Cl− ──rdeio─→ Na + Cl
"Chemical system dynamics is precisely the same as in Stochastic Petri Nets!" — with the rates computed from the current amounts:
rio — the more sodium and chloride molecules are around, the more frequently a pair ionises;rdeio.This is the law of mass action: reaction rates proportional to the product of reactant concentrations — exactly a marking-dependent rate function of an SPN. The simulation of such a system "is done by Gillespie algorithm", and the deck shows a chart of the counts of Na, Cl, Na⁺ evolving over time: the system oscillates around the equilibrium while the total number of particles stays constant — no particle is created or destroyed, only ionised and deionised.
The SPN of section 7 and the chemical network of this section are the same mathematical object: places are species, tokens are molecules, marking-dependent rates are kinetics. That is why the deck can claim a convergence of bio and ICT: simulation (Gillespie) is the shared analysis tool, and the meta-model (SPN) is the shared description language. Keep this in mind for the exam discussion: a single framework built in Scala covers mutual exclusion, producers-consumers, and biochemistry.
The last move of the lecture extends Petri Nets towards the systems that motivate the course — networks of hundreds or thousands of devices. The deck's recipe is an ad-hoc extension:
The example is a gossip algorithm on such a net, with three rules (note the rate 100000 — "quickly" — on the first):
100000
a | a ──────→ a the first transition makes sure a node holds only one copy of a
1
a | b ──────→ b the second erases tokens a where a b occurs
1
a ──────→ a | a the third spreads tokens a into neighbours
"Overall: a is gossiped in the whole network, blocked where b occurs." The a | a → a rule deduplicates (a node never keeps two copies), a | b → b makes b absorb nearby a tokens, and a → a | a is the broadcast: one token produces one copy per neighbour. In a grid, the a wavefront propagates until it meets a b region.
The full machinery — called DAP (Distributed Asynchronous Processes) in the next deck — is the meta-model behind this. A rule is pre −rateExp→ eff | ^msg: preconditions, a rate expression, effects, and an outgoing message; a token is localised in a node and characterised by an ID; the network state is tokens plus pending messages plus the neighbourhood map; and the operational semantics is a CTMC whose actions either apply a rule in a node or deliver a message with infinite rate (messages are instantaneous):
object DAP:
// Rule of the net: pre -- rateExp --> eff | ^ msg
case class Rule[P](pre: MSet[P], rateExp: MSet[P] => Double, eff: MSet[P], msg: MSet[P])
// Whole net's type
type DAP[P] = Set[Rule[P]]
// A Token, localised in a given node, characterised by an ID
case class Token[ID, P](id: ID, p: P)
// state of the network at a given time, with neighbouring as a map
case class State[ID, P](
tokens: MSet[Token[ID, P]],
messages: MSet[Token[ID, P]],
neighbours: Map[ID, Set[ID]])
// Local facility to extract the marking of a node
def localTokens[ID, P](tokens: MSet[Token[ID, P]], id: ID): MSet[P] =
tokens.collect:
case Token(`id`, t) => t // quotes are needed to match an existing variable
Its operational semantics, as a CTMC, first tries to apply every rule in every node (with the rate computed from the local marking — the neighbours' tokens stay out of reach, so the rate is local), and then delivers pending messages: each message token spreads a copy to all neighbours of its node and disappears — Action(Double.PositiveInfinity, ...), "note rate is infinity":
def toPartialFunction[ID, P](spn: DAP[P]): PartialFunction[State[ID, P], Set[Action[State[ID, P]]]] =
case State(tokens, messages, neighbours) =>
// we first try to apply rules
(for
Rule(pre, rateExp, eff, msg) <- spn // get any rule
nodeId <- neighbours.keySet // get any node
out <- tokens extract pre.map(Token(nodeId, _)) // checks if that node matches pre
newtokens = out union eff.map(Token(nodeId, _)) // generate new tokens
newmessages = messages union msg.map(Token(nodeId, _)) // generate new messages
rate = rateExp(localTokens(tokens, nodeId)) // compute rate
yield Action(rate, State(newtokens, newmessages, neighbours)))
++
(for
Token(id: ID, p: P) <- messages.asList.toSet // get any pending message
newtokens = tokens union MSet.ofList(neighbours(id).toList.map(Token(_, p))) // compute spread tokens
newmessages <- messages extract MSet(Token(id, p)) // drop the message
yield Action(Double.PositiveInfinity, State(newtokens, newmessages, neighbours))) // note rate is infinity
def toCTMC[ID, P](spn: DAP[P]): CTMC[State[ID, P]] = CTMC.ofFunction(toPartialFunction(spn))
And the gossip example from the next lecture, run on a 5×5 rectangular grid with one a token on the top-left corner — the trace prints, at each step, the time and a grid rendering of where the a tokens are:
object DAPGossip:
enum Place:
case A, B, C
type ID = (Int, Int)
export Place.*
export u08.modelling.DAP.*
export u08.modelling.DAPGrid.*
export u08.modelling.CTMCSimulation.*
val gossipRules = DAP[Place](
Rule(MSet(A, A), m => 1000, MSet(A), MSet()), // a | a -1000-> a
Rule(MSet(A), m => 1, MSet(A), MSet(A))) // a -1-> a |^ a
val gossipCTMC = DAP.toCTMC[ID, Place](gossipRules)
val net = Grids.createRectangularGrid(5, 5)
// an 'a' initial on top LEFT
val state = State[ID, Place](MSet(Token((0, 0), A)), MSet(), net)
@main def mainDAPGossip =
import DAPGossip.*
gossipCTMC.newSimulationTrace(state, new Random).take(50).toList.foreach: step =>
println(step._1) // print time
println(DAPGrid.simpleGridStateToString[Place](step._2, A)) // print state, i.e. A's
In DAP the rate of a rule is computed on localTokens(tokens, nodeId) — only the tokens of the node where the rule fires. This locality is what makes the model scale: the global CTMC has a huge state space (all tokens of all nodes), but each action is decided locally, which is exactly what makes Gillespie simulation feasible for networks of hundreds of nodes, and what Alchemist exploits in chapter 12.
The lab repository is the same https://github.com/mviroli/asmd-public-models, folder u07 (from Virtuale), imported in IntelliJ as an SBT project.
utils (the MSet abstraction "and also others"), modelling (the API/DSL: CTMC, CTMCSimulation as meta-meta-model, then SPN and DAP as meta-model), examples (extensional StochasticChannel; intensional by Petri Nets StochasticMutualExclusion and by DAP DAPGossip), and test. Check that all tests and checks pass and understand why.| Task | What it asks |
|---|---|
| SIMULATOR | Take the communication channel CTMC of StochasticChannelSimulation. Compute the average time at which communication is done — across n runs. Compute the relative amount of time (0% to 100%) that the system is in the FAIL state until communication is done — across n runs. Extract an API for nicely performing similar checks. (The theoretical expectation, from section 7's exam callout, is about 1.5 s from IDLE to DONE.) |
| GURU | Check the SPN module — CTMC modelling on top of Petri Nets, leading to Stochastic Petri Nets and Stochastic Readers & Writers. Study how key parameters/rates influence the average time the system is in the read or write state. |
| CHEMIST | SPNs can simulate chemical reaction dynamics. Experiment: search the "Brussellator" chemical reaction on Wikipedia — "it oscillates! Try to come up with a chemical reaction which oscillates." |
| RANDOM-UNIT-TESTER | How do we unit-test with randomness — and how do we test at all with randomness? Think about this in general. Create a repeatable unit test for Stochastics as in utils.StochasticSpec: seed the generator, use tolerances, or make the draws deterministic values via a random monad (the chapter 7 machinery). |
| PROBABILITY-LLM | LLMs can arguably help write/improve/complete/implement/reverse-engineer standard programming languages. But are they of help in taking probability into account? "Seemingly, it shortly fails. But with the proper prompt, it might say something reasonable." Compare with chapter 4's LLM experience. |
This chapter's story is one sentence: probabilities weight outcomes, rates weight time, and both are just labels on the transition systems of chapter 9. A strong presentation picks one task (the SIMULATOR averages, the GURU parameter study, the CHEMIST oscillator) and is ready to connect it to the analysis chapter that follows — because the natural next question after "how do I simulate?" is "how do I verify?" — which is chapter 11.
Non-determinism captures the variability of outcomes but does not weight them, does not distinguish what is normal from what is exceptional, and neglects performance. The two quantitative questions are: how probable is a given event? and what is the approximate next time the event will happen?
A DTMC is ⟨S, →⟩ with → ⊆ S × [0, 1] × S: a directed graph whose edges are labelled with probabilities; write S -p→ S′. Sanity condition 1: every state is either a deadlock or has total outgoing probability 1. Sanity condition 2: there is at most one arc from S to S′.
Likelihood factors (LF) are positive real labels on transitions (→ ⊆ S × ℝ⁺ × S) meaning "double the factor, the likelihood of the event doubles". The actual probability is obtained by normalising each factor by the sum of the outgoing LFs: LF 10, 10, 30 are equivalent to probabilities 0.2, 0.2, 0.6. This makes specifications easier to compose.
Build the cumulative distribution function FX(x) = Pr(X ≤ x) (with FX: ℝ⁺₀ → [0, 1], f(0) = 0, f(∞) = 1), draw a uniform random number n in [0, 1], and return FX⁻¹(n). For a finite set of weighted outcomes the CDF is the cumulative list of weights and the draw is the first outcome whose cumulative weight reaches n.
cumulative turns weighted choices into the cumulative list; draw scales a uniform random value by the total weight and returns the first outcome whose cumulative weight is ≥ it (collectFirst). statistics repeats the draw size times and tallies counts. The test uses choices 1.0→a, 2.0→b, 3.0→c: cumulative = List((1.0,a),(3.0,b),(6.0,c)) and 10000 draws must land at 1666±500, 3333±500, 5000±500.
A synchronous model is a threshold function; a semi-synchronous model is piecewise linear; an asynchronous model increases more gradually; the "markovian" model is the negative exponential function. The semi-synchronous model attaches a positive real that is the average expected time (or rate), not an exact duration — exact durations (Timed Petri Nets) are considered too synchronous for real distributed systems.
A CTMC is ⟨S, →⟩ with → ⊆ S × ℝ⁺ × S: a directed graph whose edges carry Markovian rates. Transitions are probabilistic and timed with the exponential CDF 1 − e^(−t·r); the average time is 1/r and the memoryless property holds: P(X > s+t | X > t) = P(X > s).
It is the distribution of memoryless processes: if the event did not happen yet, nothing new is known about when it will happen in the future. Formally P(X > s+t) = P(X > s)·P(X > t), whose only continuous solution is P(X > t) = e^(−r·t) with 1/r the average. It also matches many natural phenomena (decays, email arrivals, mutations) and is used when little is known about variability.
The probability of transiting to Si is ri/R with R = r1+...+rn, and the state transits at Markovian rate R. This follows from the distribution of the minimum of exponential random variables: P(X > min(t1,...,tn)) = 1 − e^(−(r1+...+rn)·t). The probability part of a CTMC is the same as a DTMC with normalisation; a DTMC equates a CTMC if durations are forgotten.
Take the next states and rates (S1, r1), ..., (Sn, rn) from the current state; let R be the sum of rates; choose the next state Si with probability ri/R using a random number τ1; draw Δt from the exponential distribution with rate R using a random number τ2: Δt = (1/R)·log(1/τ2). Traces have the form [S0s, t0s], [S1s, t1s], ..., [Sns, tns].
Trace[A] = LazyList[Event[A]] with Event(time, state). newSimulationTrace(s0, rnd) uses LazyList.iterate(Event(0.0, s0)): if the state has no transitions it repeats itself; otherwise it maps transitions to (rate, state) pairs, cumulates them, draws the next state, and advances the time by Math.log(1/rnd.nextDouble())/sumR.
Trn gains a rate function: Trn(cond, rate: MSet[P] => Double, eff, inh). toCTMC compiles an SPN into a CTMC[MSet[P]]: for each transition, check inhibition (m disjoined inh), compute r = rate(m) on the current marking, extract the precondition and yield Action(r, out union eff). The enablement machinery is unchanged from chapter 9.
Stochastic mutual exclusion: N→T at rate 1.0, T→C at rate m(T) (marking-dependent: more trying processes, faster entry), C→sink at rate 2.0, with the inhibitor on T→C. Stochastic readers/writers: t1: 1.0 (arrival), t2: 200000 (66% readers), t3: 100000 (33% writers), t4,t5: 100000 (immediate), t6: 0.1·p6 (reading), t7: 0.2 (writing).
Chemical reactions are transitions whose rates follow the law of mass action — proportional to the product of reactant counts — which is exactly a marking-dependent rate function of an SPN. Example: Na + Cl −rio→ Na+ + Cl− with ionization rate #Na·#Cl·rio. Simulation by Gillespie; the deck concludes "Chemical system dynamics is precisely the same as in Stochastic Petri Nets!"
The firing arc is a special outgoing arc that broadcasts a token to all neighbours, on a network where each device is the same Petri Net. The rules: a|a −1000→ a deduplicates (quickly one copy per node), a|b −1→ b erases a where b occurs, a −1→ a|a spreads a to neighbours. Overall a is gossiped in the whole network, blocked where b occurs. In DAP, messages are delivered with infinite rate.