Tutorial 17: A colored generalized stochastic Petri net

A generalized stochastic Petri net (GSPN) admits two kinds of transition: timed ones, which fire after a random delay, and immediate ones, which fire as soon as they are enabled and consume no time at all. It becomes colored when the tokens carry a type. In LINE a token color is simply a job class, so a place holds one marking per color and a transition declares one mode per color it serves, with its own firing law, enabling arcs and inhibitor arcs.

This tutorial builds the smallest net that uses all of it. Two colors, Gold and Silver, two tokens of each, circulate between a Buffer place and a Server place. The immediate transition admit has one mode per color and moves a waiting token into the server; both of its modes carry inhibitor arcs on both colors, so a token is admitted only into an empty server and the server becomes a mutual-exclusion resource. When both colors are waiting, which here is always, the conflict is resolved by the firing weights: Gold carries weight 2 against Silver's 1. The timed transition serve also has one mode per color and returns the token to the buffer at a color-dependent rate, 3 for Gold against 1.5 for Silver.

One ordering matters: every node is declared before any of it is parameterized. The enabling, inhibiting and firing matrices a mode carries are indexed by (node, class) and are sized against the node count of the model as it stands, so declaring a place after setting a transition's conditions leaves those conditions a row short.

Colored GSPN Diagram
model = Network('ColoredGSPN');
% Block 1: declare every node before parameterizing any of it -- the enabling,
% inhibiting and firing matrices are indexed by (node, class)
buf   = Place(model, 'Buffer');
srv   = Place(model, 'Server');
admit = Transition(model, 'admit');
serve = Transition(model, 'serve');
% Block 2: two token colors, both starting in the buffer
gold   = ClosedClass(model, 'Gold',   2, buf, 0);
silver = ClosedClass(model, 'Silver', 2, buf, 0);
% Block 3: the immediate transition, one mode per color. The inhibitor arcs on
% BOTH colors make the server exclusive; the weights set the color mix
mg = admit.addMode('gold');
admit.setTimingStrategy(mg, TimingStrategy.IMMEDIATE);
admit.setEnablingConditions(mg, gold, buf, 1);
admit.setInhibitingConditions(mg, gold, srv, 1);
admit.setInhibitingConditions(mg, silver, srv, 1);
admit.setFiringOutcome(mg, gold, srv, 1);
admit.setFiringWeights(mg, 2.0);
ms = admit.addMode('silver');
admit.setTimingStrategy(ms, TimingStrategy.IMMEDIATE);
admit.setEnablingConditions(ms, silver, buf, 1);
admit.setInhibitingConditions(ms, gold, srv, 1);
admit.setInhibitingConditions(ms, silver, srv, 1);
admit.setFiringOutcome(ms, silver, srv, 1);
admit.setFiringWeights(ms, 1.0);
% Block 4: the timed transition, one mode per color, color-dependent rates
sg = serve.addMode('gold');
serve.setDistribution(sg, Exp(3.0));
serve.setEnablingConditions(sg, gold, srv, 1);
serve.setFiringOutcome(sg, gold, buf, 1);
ss = serve.addMode('silver');
serve.setDistribution(ss, Exp(1.5));
serve.setEnablingConditions(ss, silver, srv, 1);
serve.setFiringOutcome(ss, silver, buf, 1);
% Block 5: topology and initial marking
P = model.initRoutingMatrix();
for c = {gold, silver}
    P.set(c{1}, c{1}, buf,   admit, 1.0);
    P.set(c{1}, c{1}, admit, srv,   1.0);
    P.set(c{1}, c{1}, srv,   serve, 1.0);
    P.set(c{1}, c{1}, serve, buf,   1.0);
end
model.link(P);
buf.setState([2, 2]);
srv.setState([0, 0]);
% Block 6: exact solution, and a simulation cross-check
AvgTableCTMC = SolverCTMC(model, 'cutoff', 4).avgTable()
AvgTableSSA = SolverSSA(model, 'seed', 23000, 'samples', 2e5).avgTable()
Network model = new Network("ColoredGSPN");
// Block 1: declare every node before parameterizing any of it -- the enabling,
// inhibiting and firing matrices are indexed by (node, class)
Place buf = new Place(model, "Buffer");
Place srv = new Place(model, "Server");
Transition admit = new Transition(model, "admit");
Transition serve = new Transition(model, "serve");
// Block 2: two token colors, both starting in the buffer
ClosedClass gold = new ClosedClass(model, "Gold", 2, buf, 0);
ClosedClass silver = new ClosedClass(model, "Silver", 2, buf, 0);
// Block 3: the immediate transition, one mode per color. The inhibitor arcs on
// BOTH colors make the server exclusive; the weights set the color mix
Mode mg = admit.addMode("gold");
admit.setTimingStrategy(mg, TimingStrategy.IMMEDIATE);
admit.setEnablingConditions(mg, gold, buf, 1);
admit.setInhibitingConditions(mg, gold, srv, 1);
admit.setInhibitingConditions(mg, silver, srv, 1);
admit.setFiringOutcome(mg, gold, srv, 1);
admit.setFiringWeights(mg, 2.0);
Mode ms = admit.addMode("silver");
admit.setTimingStrategy(ms, TimingStrategy.IMMEDIATE);
admit.setEnablingConditions(ms, silver, buf, 1);
admit.setInhibitingConditions(ms, gold, srv, 1);
admit.setInhibitingConditions(ms, silver, srv, 1);
admit.setFiringOutcome(ms, silver, srv, 1);
admit.setFiringWeights(ms, 1.0);
// Block 4: the timed transition, one mode per color, color-dependent rates
Mode sg = serve.addMode("gold");
serve.setDistribution(sg, new Exp(3.0));
serve.setEnablingConditions(sg, gold, srv, 1);
serve.setFiringOutcome(sg, gold, buf, 1);
Mode ss = serve.addMode("silver");
serve.setDistribution(ss, new Exp(1.5));
serve.setEnablingConditions(ss, silver, srv, 1);
serve.setFiringOutcome(ss, silver, buf, 1);
// Block 5: topology and initial marking
RoutingMatrix P = model.initRoutingMatrix();
for (JobClass c : new JobClass[]{gold, silver}) {
    P.set(c, c, buf, admit, 1.0);
    P.set(c, c, admit, srv, 1.0);
    P.set(c, c, srv, serve, 1.0);
    P.set(c, c, serve, buf, 1.0);
}
model.link(P);
Matrix marking = new Matrix(1, 2);
marking.set(0, 0, 2);
marking.set(0, 1, 2);
buf.setState(marking);
srv.setState(new Matrix(1, 2));
// Block 6: exact solution, and a simulation cross-check
new CTMC(model, "cutoff", 4).getAvgTable().print();
new SSA(model, "seed", 23000, "samples", 200000).getAvgTable().print();
model = Network('ColoredGSPN')
# Block 1: declare every node before parameterizing any of it -- the enabling,
# inhibiting and firing matrices are indexed by (node, class)
buf = Place(model, 'Buffer')
srv = Place(model, 'Server')
admit = Transition(model, 'admit')
serve = Transition(model, 'serve')
# Block 2: two token colors, both starting in the buffer
gold = ClosedClass(model, 'Gold', 2, buf, 0)
silver = ClosedClass(model, 'Silver', 2, buf, 0)
# Block 3: the immediate transition, one mode per color. The inhibitor arcs on
# BOTH colors make the server exclusive; the weights set the color mix
mg = admit.add_mode('gold')
admit.set_timing_strategy(mg, TimingStrategy.IMMEDIATE)
admit.set_enabling_conditions(mg, gold, buf, 1)
admit.set_inhibiting_conditions(mg, gold, srv, 1)
admit.set_inhibiting_conditions(mg, silver, srv, 1)
admit.set_firing_outcome(mg, gold, srv, 1)
admit.set_firing_weights(mg, 2.0)
ms = admit.add_mode('silver')
admit.set_timing_strategy(ms, TimingStrategy.IMMEDIATE)
admit.set_enabling_conditions(ms, silver, buf, 1)
admit.set_inhibiting_conditions(ms, gold, srv, 1)
admit.set_inhibiting_conditions(ms, silver, srv, 1)
admit.set_firing_outcome(ms, silver, srv, 1)
admit.set_firing_weights(ms, 1.0)
# Block 4: the timed transition, one mode per color, color-dependent rates
sg = serve.add_mode('gold')
serve.set_distribution(sg, Exp(3.0))
serve.set_enabling_conditions(sg, gold, srv, 1)
serve.set_firing_outcome(sg, gold, buf, 1)
ss = serve.add_mode('silver')
serve.set_distribution(ss, Exp(1.5))
serve.set_enabling_conditions(ss, silver, srv, 1)
serve.set_firing_outcome(ss, silver, buf, 1)
# Block 5: topology and initial marking
P = model.init_routing_matrix()
for c in (gold, silver):
    P.set(c, c, buf, admit, 1.0)
    P.set(c, c, admit, srv, 1.0)
    P.set(c, c, srv, serve, 1.0)
    P.set(c, c, serve, buf, 1.0)
model.link(P)
buf.set_state([2, 2])
srv.set_state([0, 0])
# Block 6: exact solution, and a simulation cross-check
print(SolverCTMC(model, cutoff=4).avg_table())
print(SolverSSA(model, seed=23000, samples=200000).avg_table())
// A mode's arcs are (node, class) entries, so a color is the class index of an
// arc. The matrices are sized against the FULL node count of the finished
// model, which is why nn counts the two transitions as well.
Network model("ColoredGSPN");
Place buf(model, "Buffer");
Place srv(model, "Server");
ClosedClass gold(model, "Gold", 2.0, buf, 0);
ClosedClass silver(model, "Silver", 2.0, buf, 0);
const std::size_t nn = 4;  // Buffer, Server, admit, serve
// The immediate transition, one mode per color
Mode ag;
ag.name = "gold";
ag.timing = TimingStrategy::IMMEDIATE;
ag.weight = 2.0;
ag.enab.push_back(Arc{buf, 1.0, gold});
ag.inhib.push_back(Arc{srv, 1.0, gold});
ag.inhib.push_back(Arc{srv, 1.0, silver});
ag.fire.push_back(Arc{srv, 1.0, gold});
Mode as;
as.name = "silver";
as.timing = TimingStrategy::IMMEDIATE;
as.weight = 1.0;
as.enab.push_back(Arc{buf, 1.0, silver});
as.inhib.push_back(Arc{srv, 1.0, gold});
as.inhib.push_back(Arc{srv, 1.0, silver});
as.fire.push_back(Arc{srv, 1.0, silver});
std::vector<Mode> admitmodes;
admitmodes.push_back(ag);
admitmodes.push_back(as);
const std::size_t admit = model.add_transition("admit", trans(nn, admitmodes, 2));
// The timed transition, one mode per color, color-dependent rates
Mode sg;
sg.name = "gold";
sg.proc = Exp(3.0);
sg.enab.push_back(Arc{srv, 1.0, gold});
sg.fire.push_back(Arc{buf, 1.0, gold});
Mode ss;
ss.name = "silver";
ss.proc = Exp(1.5);
ss.enab.push_back(Arc{srv, 1.0, silver});
ss.fire.push_back(Arc{buf, 1.0, silver});
std::vector<Mode> servemodes;
servemodes.push_back(sg);
servemodes.push_back(ss);
const std::size_t serve = model.add_transition("serve", trans(nn, servemodes, 2));
// Topology and initial marking
Routing P;
P.set(gold, gold, buf, admit, 1.0);
P.set(silver, silver, buf, admit, 1.0);
P.set(gold, gold, admit, srv, 1.0);
P.set(silver, silver, admit, srv, 1.0);
P.set(gold, gold, srv, serve, 1.0);
P.set(silver, silver, srv, serve, 1.0);
P.set(gold, gold, serve, buf, 1.0);
P.set(silver, silver, serve, buf, 1.0);
model.link(P);
buf.set_initial_marking(std::vector<double>{2.0, 2.0});
srv.set_initial_marking(std::vector<double>{0.0, 0.0});
// Exact solution, and a simulation cross-check
SolverOpts ctmc;
ctmc.cutoff = 4;
print_avg(solve_avg("CTMC", model, ctmc));
SolverOpts ssa;
ssa.samples = 200000;
ssa.seed = 23000;
print_avg(solve_avg("SSA", model, ssa));

Expected Output

The immediate transition is removed before the chain is solved: stochastic complementation eliminates the vanishing markings, here 136 of them, and leaves 8 tangible states that SolverCTMC solves exactly. The simulation reproduces them:

AvgTableCTMC =                      % exact, by stochastic complementation
    Station    JobClass    QLen    RespT      Tput
    _______    ________    ____    _______    ____
     Buffer     Gold        1.5          1     1.5
     Buffer     Silver      1.5          2    0.75
     Server     Gold        0.5    0.33333     1.5
     Server     Silver      0.5    0.66667    0.75
AvgTableSSA =                       % 2e5 samples, seed 23000
     Buffer     Gold     1.5003     1.0007    1.4992
     Buffer     Silver   1.4997     1.9987   0.75038
     Server     Gold    0.49975    0.33333    1.4992
     Server     Silver  0.50025    0.66667   0.75038

Every number here can be checked by hand, which is the point of building the net this way. Admission costs no time, so the server is never idle and the two utilizations sum to one. Both colors always have a token waiting, so every admission is a weighted draw and the completions split in the ratio of the firing weights, twice as many Gold as Silver. The mean service time is therefore (2/3)(1/3) + (1/3)(2/3) = 4/9, so the two colors complete at 9/4 = 2.25 jobs per unit time in total, 1.5 Gold and 0.75 Silver, and each color holds the server half the time. What remains, 1.5 tokens of each color, is waiting in the buffer. Change one firing weight and the whole table moves with it: at equal weights the two colors complete at 1 each instead, and the buffer holds 1.667 Gold against 1.333 Silver.