Tutorial 13: Open Cluster

An open cluster: a single open class flows from a Source through a dispatcher (Router) into one of M=3 parallel servers, then leaves through a Sink. The tutorial shows two equivalent ways of building the model and compares two dispatching policies on the same farm.

Block 1: One-liner factory

The arrival rate is λ = 0.4 and each PS server has a mean service time of D = 1, giving a per-server utilization of λ/M ≈ 0.133 under random dispatching.

lambda = 0.4;
D = ones(3,1);
model = Network.clusterPs(lambda, D, RoutingStrategy.RAND);
avgTable = MVA(model).getAvgTable()
Matrix lambda = new Matrix("[0.4]");
Matrix D = new Matrix("[1.0; 1.0; 1.0]");
Network model = Network.clusterPs(lambda, D, RoutingStrategy.RAND);
new MVA(model).getAvgTable().print();
val lambda = Matrix("[0.4]")
val D = Matrix("[1.0; 1.0; 1.0]")
val model = Network.clusterPs(lambda, D, RoutingStrategy.RAND)
MVA(model).getAvgTable().print()
from line_solver import *

lam = [0.4]
D = [[1.0], [1.0], [1.0]]
model = Network.cluster_ps(lam, D, RoutingStrategy.RAND)
print(MVA(model).get_avg_table())

Expected MVA output

    Station    JobClass     QLen       Util      RespT     ResidT      ArvR       Tput
    _______    ________    _______    _______    ______    _______    _______    _______
    Source      Class1           0          0         0          0          0        0.4
    Server1     Class1     0.15263    0.13333    1.1448    0.38158    0.13333    0.13333
    Server2     Class1     0.15263    0.13333    1.1448    0.38158    0.13333    0.13333
    Server3     Class1     0.15263    0.13333    1.1448    0.38158    0.13333    0.13333

Block 2: Cluster builder with multi-server queues

Switch to the chainable Cluster builder to introduce non-uniform server multiplicities (Server 1 becomes M/M/2, the others stay M/M/1) and replace PS with FCFS scheduling:

farm = Cluster().setNumStations(3).setArrivalRate(0.4).setServiceRate(1.0);
farm.setScheduling(SchedStrategy.FCFS);
farm.setStationCounts([2; 1; 1]);   % Server1 = M/M/2, Server2/3 = M/M/1
avgTableFcfs = MVA(farm.build()).getAvgTable()
Cluster farm = new Cluster().setNumStations(3).setArrivalRate(0.4).setServiceRate(1.0)
        .setScheduling(SchedStrategy.FCFS)
        .setStationCounts(new int[] {2, 1, 1});
new MVA(farm.build()).getAvgTable().print();
val farm = Cluster().setNumStations(3).setArrivalRate(0.4).setServiceRate(1.0)
        .setScheduling(SchedStrategy.FCFS)
        .setStationCounts(intArrayOf(2, 1, 1))
MVA(farm.build()).getAvgTable().print()
farm = (Cluster().set_num_stations(3).set_arrival_rate(0.4).set_service_rate(1.0)
        .set_scheduling(SchedStrategy.FCFS)
        .set_station_counts([2, 1, 1]))
print(MVA(farm.build()).get_avg_table())

Block 3: Cross-check JMT, LDES, and SSA on the FCFS multi-cluster

The same model is solved under three simulators. JMT is the XML-driven Java Modelling Tools simulator; LDES is the LINE Discrete Event Simulator built on the SSJ library, invoked as a subprocess; SSA is LINE's native next-reaction-method stochastic simulator. All three produce statistically equivalent results on this open-class farm.

disp(JMT(farm.build(),  'seed', 23000, 'samples', 20000).getAvgTable());
disp(LDES(farm.build(), 'seed', 23000, 'samples', 20000).getAvgTable());
disp(SSA(farm.build(),  'seed', 23000, 'samples', 20000).getAvgTable());
new JMT(farm.build(),  "seed", 23000, "samples", 20000).getAvgTable().print();
new LDES(farm.build(), "seed", 23000, "samples", 20000).getAvgTable().print();
new SSA(farm.build(),  "seed", 23000, "samples", 20000).getAvgTable().print();
JMT(farm.build(),  "seed", 23000, "samples", 20000).getAvgTable().print()
LDES(farm.build(), "seed", 23000, "samples", 20000).getAvgTable().print()
SSA(farm.build(),  "seed", 23000, "samples", 20000).getAvgTable().print()
print(JMT(farm.build(),  seed=23000, samples=20000).get_avg_table())
print(LDES(farm.build(), seed=23000, samples=20000).get_avg_table())
print(SSA(farm.build(),  seed=23000, samples=20000).get_avg_table())

Block 4: Compare dispatching policies

Round-robin dispatching is non-product-form, so we drop to JMT with a small sample budget. The compareDispatching helper runs the same farm under each policy and returns one result table per policy.

farm2 = Cluster().setNumStations(3).setArrivalRate(0.4).setServiceRate(1.0);
farm2.setScheduling(SchedStrategy.PS);
solverFcn = @(m) JMT(m, 'seed', 23000, 'samples', 5000).getAvgTable();
results = farm2.compareDispatching(solverFcn, ...
    [RoutingStrategy.RAND, RoutingStrategy.RROBIN]);

keys_ = results.keys;
for k = 1:numel(keys_)
    fprintf('\n=== Dispatching: %s ===\n', keys_{k});
    disp(results(keys_{k}));
end
Cluster farm2 = new Cluster().setNumStations(3).setArrivalRate(0.4).setServiceRate(1.0)
        .setScheduling(SchedStrategy.PS);
Function<Network, NetworkAvgTable> solverFcn =
        m -> new JMT(m, "seed", 23000, "samples", 5000).getAvgTable();
farm2.compareDispatching(solverFcn,
        RoutingStrategy.RAND, RoutingStrategy.RROBIN)
     .forEach((policy, table) -> {
         System.out.println("=== Dispatching: " + policy + " ===");
         table.print();
     });
val farm2 = Cluster().setNumStations(3).setArrivalRate(0.4).setServiceRate(1.0).setScheduling(SchedStrategy.PS)
val solverFcn: (Network) -> NetworkAvgTable =
        { m -> JMT(m, "seed", 23000, "samples", 5000).getAvgTable() }
farm2.compareDispatching(solverFcn,
        RoutingStrategy.RAND, RoutingStrategy.RROBIN)
     .forEach { (policy, table) ->
         println("=== Dispatching: $policy ===")
         table.print()
     }
farm2 = (Cluster().set_num_stations(3).set_arrival_rate(0.4).set_service_rate(1.0)
         .set_scheduling(SchedStrategy.PS))
solver_fcn = lambda m: JMT(m, seed=23000, samples=5000).get_avg_table()
for policy in (RoutingStrategy.RAND, RoutingStrategy.RROBIN):
    farm2.set_dispatching(policy)
    print(f"=== Dispatching: {policy} ===")
    print(solver_fcn(farm2.build()))

For this lightly loaded farm both policies produce very similar response times; differences become visible only at higher utilization, where round-robin reduces variability across servers compared with the i.i.d. split produced by random dispatching.