Open Jackson Network Model from JSON

On this page we will create a ciw network model from a specification in a JSON file.

The model represents a a classic queuing network problem that can be formulated as an Open Jackson Network.

Jackson network

1. Imports

1.1 json2ciw imports

We will use:

  • load_jackson_network_model function that loads the built in urgent care call centre JSON file.
  • ProcessModel: a pydantic schema that provides automatic validation of the JSON.
  • CiwConverter that accepts a valid ProcessModel that represents a DES model and returns a ciw parameter dict.
  • multiple_replications:runs the network model and results a Dataframe of replication results.
  • summarise_results: provides a formatted table of mean results for each node in the network.
  • tidy_to_wide_format: the results from multiple_replications are returned in tidy (stacked row) format. This function converts into wide format i.e. each column is a performance measure.
  • create_user_filtered_hist generates a interactive plotly chart to view replications for each performance measure. It requires replication data in wide format.
from json2ciw.datasets import load_jackson_network_model
from json2ciw.engine import (
    CiwConverter,
    multiple_replications
)
from json2ciw.results import (
    summarise_results, 
    create_user_filtered_hist,
    tidy_to_wide_format
)

from json2ciw.schema import ProcessModel

1.2 Other imports

import ciw
import statistics
from IPython.display import JSON
from rich import print

2. Load JSON

json_network = load_jackson_network_model()

display as collapsible JSON. Expand to view the details.

JSON(json_network)
<IPython.core.display.JSON object>

3. Convert to a ProcessModel

A ProcessModel is a pydantic schema. It is independent of ciw. This early version will automatically validate that the JSON file is correct and that all transitions add up to 1.0 when it is created.

model_instance = ProcessModel(**json_network)

The contents of the process model can be manually inspected by a developer as follows:

print(model_instance)
ProcessModel(
    name='Open Jackson Network',
    description='A simple jackson network to convert to ciw',
    activities=[
        Activity(
            name='Service 1',
            type='activity',
            resource=Resource(name='Servers 1', capacity=1),
            service_distribution=Distribution(type='exponential', parameters={'mean': 0.1}),
            arrival_distribution=Distribution(type='exponential', parameters={'rate': 1.0}),
            renege_distribution=None
        ),
        Activity(
            name='Service 2',
            type='activity',
            resource=Resource(name='Servers 2', capacity=2),
            service_distribution=Distribution(type='exponential', parameters={'mean': 0.1}),
            arrival_distribution=Distribution(type='exponential', parameters={'rate': 4.0}),
            renege_distribution=None
        ),
        Activity(
            name='Service 3',
            type='activity',
            resource=Resource(name='Servers 3', capacity=1),
            service_distribution=Distribution(type='exponential', parameters={'mean': 0.1}),
            arrival_distribution=Distribution(type='exponential', parameters={'rate': 3.0}),
            renege_distribution=None
        )
    ],
    transitions=[
        Transition(source='Service 1', target='Exit', probability=0.1),
        Transition(source='Service 1', target='Service 2', probability=0.6),
        Transition(source='Service 1', target='Service 3', probability=0.3),
        Transition(source='Service 2', target='Exit', probability=0.6),
        Transition(source='Service 2', target='Service 1', probability=0.1),
        Transition(source='Service 2', target='Service 3', probability=0.3),
        Transition(source='Service 3', target='Exit', probability=0.2),
        Transition(source='Service 3', target='Service 2', probability=0.4),
        Transition(source='Service 3', target='Service 1', probability=0.4)
    ]
)
model_instance.display_diagram(include_resources=False)
graph TD
    %% Open Jackson Network: A simple jackson network to convert to ciw
    Arrivals_Service_1("Time between arrivals<br/>Exponential(λ=1.0)")
    Arrivals_Service_2("Time between arrivals<br/>Exponential(λ=4.0)")
    Arrivals_Service_3("Time between arrivals<br/>Exponential(λ=3.0)")
    Service_1["Service 1<br/>Exponential(mean=0.1)"]
    Service_2["Service 2<br/>Exponential(mean=0.1)"]
    Service_3["Service 3<br/>Exponential(mean=0.1)"]
    Exit(["Exit"])

    Arrivals_Service_1 --> Service_1
    Arrivals_Service_2 --> Service_2
    Arrivals_Service_3 --> Service_3
    Service_1 -->|10%| Exit
    Service_1 -->|60%| Service_2
    Service_1 -->|30%| Service_3
    Service_2 -->|60%| Exit
    Service_2 -->|10%| Service_1
    Service_2 -->|30%| Service_3
    Service_3 -->|20%| Exit
    Service_3 -->|40%| Service_2
    Service_3 -->|40%| Service_1
model_instance.display_diagram(include_resources=True)
graph TD
    %% Open Jackson Network: A simple jackson network to convert to ciw
    Arrivals_Service_1("Time between arrivals<br/>Exponential(λ=1.0)")
    Arrivals_Service_2("Time between arrivals<br/>Exponential(λ=4.0)")
    Arrivals_Service_3("Time between arrivals<br/>Exponential(λ=3.0)")
    Service_1["Service 1<br/>Exponential(mean=0.1)"]
    Service_2["Service 2<br/>Exponential(mean=0.1)"]
    Service_3["Service 3<br/>Exponential(mean=0.1)"]
    Resource_Servers_1(("Servers 1<br/>(1)"))
    Resource_Servers_2(("Servers 2<br/>(2)"))
    Resource_Servers_3(("Servers 3<br/>(1)"))
    Exit(["Exit"])

    Arrivals_Service_1 --> Service_1
    Arrivals_Service_2 --> Service_2
    Arrivals_Service_3 --> Service_3
    Resource_Servers_1 -.Seize.-> Service_1
    Service_1 -.Release.-> Resource_Servers_1
    Resource_Servers_2 -.Seize.-> Service_2
    Service_2 -.Release.-> Resource_Servers_2
    Resource_Servers_3 -.Seize.-> Service_3
    Service_3 -.Release.-> Resource_Servers_3
    Service_1 -->|10%| Exit
    Service_1 -->|60%| Service_2
    Service_1 -->|30%| Service_3
    Service_2 -->|60%| Exit
    Service_2 -->|10%| Service_1
    Service_2 -->|30%| Service_3
    Service_3 -->|20%| Exit
    Service_3 -->|40%| Service_2
    Service_3 -->|40%| Service_1
# summary of distributions
model_instance.get_distributions_df()
Activity Phase Distribution Type Parameters
0 Service 1 Arrival Exponential rate=1.0
1 Service 1 Service Exponential mean=0.1
2 Service 2 Arrival Exponential rate=4.0
3 Service 2 Service Exponential mean=0.1
4 Service 3 Arrival Exponential rate=3.0
5 Service 3 Service Exponential mean=0.1
# summary of routing percentages
model_instance.get_routing_matrix_df()
Service 1 Service 2 Service 3 Exit
Source Activity
Service 1 0.0 0.6 0.3 0.1
Service 2 0.1 0.0 0.3 0.6
Service 3 0.4 0.4 0.0 0.2
model_instance.get_resources_df()
Resource Activity Count
0 Servers 1 Service 1 1
1 Servers 2 Service 2 2
2 Servers 3 Service 3 1

3. Convert to a ciw Network Model

adapter = CiwConverter(model_instance)
network_params = adapter.generate_params()

The variable network_params is a python dict that contains all the parameters that ciw requires for a very simple queuing model. This can be passed as keyword args to the ciw.create_network function.

network_params
{'number_of_servers': [1, 2, 1],
 'arrival_distributions': [Exponential(rate=1.0),
  Exponential(rate=4.0),
  Exponential(rate=3.0)],
 'service_distributions': [Exponential(rate=10.0),
  Exponential(rate=10.0),
  Exponential(rate=10.0)],
 'reneging_time_distributions': [None, None, None],
 'routing': [[0.0, 0.6, 0.3], [0.1, 0.0, 0.3], [0.4, 0.4, 0.0]]}
# we use ciw's native `create_network` method
network = ciw.create_network(**network_params)
print(type(network))
<class 'ciw.network.Network'>
# Run a quick simulation to verify it works without crashing...
sim = ciw.Simulation(network)
sim.simulate_until_max_time(50)
print("Quick simulation run worked!")
Quick simulation run worked!

4. Run the model for multiple replications

The multiple_replications function returns a DataFrame that contains one row per activity per replication. So if there are two nodes there are two rows for each replication.

# Run replications with activity/resource names
df_reps = multiple_replications(
    network, 
    model_instance, 
    num_reps=100, 
    runtime=2880,
    warmup=1440,
    n_jobs=-1 # parallel reps
)

df_reps.head()
rep node_id activity_name resource_name resource_capacity n_service mean_wait mean_service utilisation mean_Lq
0 0 1 Service 1 Servers 1 1 7235 0.100526 0.101436 49.753370 0.505073
1 0 2 Service 2 Servers 2 2 14480 0.035334 0.101033 49.936752 0.355298
2 0 3 Service 3 Servers 3 1 10834 0.311941 0.101618 75.011634 2.346920
3 1 1 Service 1 Servers 1 1 7221 0.105431 0.100141 49.728757 0.528695
4 1 2 Service 2 Servers 2 2 14483 0.034044 0.100673 50.070456 0.342399

Filter the nodes with the usual pandas syntax

df_reps.loc[df_reps['activity_name'] == "Service 1"].head(3)
rep node_id activity_name resource_name resource_capacity n_service mean_wait mean_service utilisation mean_Lq
0 0 1 Service 1 Servers 1 1 7235 0.100526 0.101436 49.753370 0.505073
3 1 1 Service 1 Servers 1 1 7221 0.105431 0.100141 49.728757 0.528695
6 2 1 Service 1 Servers 1 1 7213 0.099478 0.099994 49.805521 0.498286

5. Summarise results

If needed there is a built in function to create a DataFrame that contains a mean summary of all metrics across the nodes.

# Get summary table
summary = summarise_results(df_reps)
summary.round(1)
activity Metric Service 1 (Servers 1) Service 2 (Servers 2) Service 3 (Servers 3)
0 Mean completed services 7187.8 14414.0 10795.3
1 Mean waiting time 0.1 0.0 0.3
2 Mean service time 0.1 0.1 0.1
3 Mean utilisation 50.0 50.0 75.0
4 Mean queue length 0.5 0.3 2.2