Open Network with lognormal service times

In this example, the model represents a simple three node network problem with lognormal service times.

The JSON for this built-in example can be loaded using json2ciw.datasets.load_three_node_network_model.

Imports

import json

import ciw
from rich import print

from json2ciw.datasets import load_three_node_network_model
from json2ciw.engine import CiwConverter, multiple_replications
from json2ciw.results import summarise_results, tidy_to_wide_format
from json2ciw.schema import ProcessModel

Load JSON

json_network = load_three_node_network_model()
print(json.dumps(json_network, indent=2))
{
  "name": "Three Node Network",
  "description": "A simple three node network to convert to ciw",
  "activities": [
    {
      "name": "Node 1",
      "type": "activity",
      "resource": {
        "name": "Resource 1",
        "capacity": 18
      },
      "service_distribution": {
        "type": "lognormal",
        "parameters": {
          "mean": 1,
          "std": 0.1
        }
      },
      "arrival_distribution": {
        "type": "exponential",
        "parameters": {
          "rate": 1.0
        }
      }
    },
    {
      "name": "Node 2",
      "type": "activity",
      "resource": {
        "name": "Resource 2",
        "capacity": 12
      },
      "service_distribution": {
        "type": "normal",
        "parameters": {
          "mean": 1,
          "std": 0.1
        }
      },
      "arrival_distribution": {
        "type": "exponential",
        "parameters": {
          "rate": 4.0
        }
      }
    },
    {
      "name": "Node 3",
      "type": "activity",
      "resource": {
        "name": "Resource 3",
        "capacity": 25
      },
      "service_distribution": {
        "type": "lognormal",
        "parameters": {
          "mean": 1,
          "std": 0.1
        }
      },
      "arrival_distribution": {
        "type": "exponential",
        "parameters": {
          "rate": 3.0
        }
      }
    }
  ],
  "transitions": [
    {
      "from": "Node 1",
      "to": "Exit",
      "probability": 0.1
    },
    {
      "from": "Node 1",
      "to": "Node 2",
      "probability": 0.6
    },
    {
      "from": "Node 1",
      "to": "Node 3",
      "probability": 0.3
    },
    {
      "from": "Node 2",
      "to": "Exit",
      "probability": 0.6
    },
    {
      "from": "Node 2",
      "to": "Node 1",
      "probability": 0.1
    },
    {
      "from": "Node 2",
      "to": "Node 3",
      "probability": 0.3
    },
    {
      "from": "Node 3",
      "to": "Exit",
      "probability": 0.2
    },
    {
      "from": "Node 3",
      "to": "Node 2",
      "probability": 0.4
    },
    {
      "from": "Node 3",
      "to": "Node 1",
      "probability": 0.4
    }
  ]
}

Validate with ProcessModel

model_instance = ProcessModel(**json_network)
print(model_instance)
ProcessModel(
    name='Three Node Network',
    description='A simple three node network to convert to ciw',
    activities=[
        Activity(
            name='Node 1',
            type='activity',
            resource=Resource(name='Resource 1', capacity=18),
            service_distribution=Distribution(type='lognormal', parameters={'mean': 1.0, 'std': 0.1}),
            arrival_distribution=Distribution(type='exponential', parameters={'rate': 1.0}),
            renege_distribution=None
        ),
        Activity(
            name='Node 2',
            type='activity',
            resource=Resource(name='Resource 2', capacity=12),
            service_distribution=Distribution(type='normal', parameters={'mean': 1.0, 'std': 0.1}),
            arrival_distribution=Distribution(type='exponential', parameters={'rate': 4.0}),
            renege_distribution=None
        ),
        Activity(
            name='Node 3',
            type='activity',
            resource=Resource(name='Resource 3', capacity=25),
            service_distribution=Distribution(type='lognormal', parameters={'mean': 1.0, 'std': 0.1}),
            arrival_distribution=Distribution(type='exponential', parameters={'rate': 3.0}),
            renege_distribution=None
        )
    ],
    transitions=[
        Transition(source='Node 1', target='Exit', probability=0.1),
        Transition(source='Node 1', target='Node 2', probability=0.6),
        Transition(source='Node 1', target='Node 3', probability=0.3),
        Transition(source='Node 2', target='Exit', probability=0.6),
        Transition(source='Node 2', target='Node 1', probability=0.1),
        Transition(source='Node 2', target='Node 3', probability=0.3),
        Transition(source='Node 3', target='Exit', probability=0.2),
        Transition(source='Node 3', target='Node 2', probability=0.4),
        Transition(source='Node 3', target='Node 1', probability=0.4)
    ]
)
model_instance.save_diagram("urgentcare.mmd", include_resources=False)

flowchart TD
    Arrivals_Node_1("Time between arrivals<br/>Exponential(λ=1.0)")
    Arrivals_Node_2("Time between arrivals<br/>Exponential(λ=4.0)")
    Arrivals_Node_3("Time between arrivals<br/>Exponential(λ=3.0)")
    Node_1["Node 1<br/>Lognormal(mean=1.0, stdev=0)"]
    Node_2["Node 2<br/>Normal(mean=1.0, sd=0)"]
    Node_3["Node 3<br/>Lognormal(mean=1.0, stdev=0)"]
    Exit(["Exit"])

    Arrivals_Node_1 --> Node_1
    Arrivals_Node_2 --> Node_2
    Arrivals_Node_3 --> Node_3
    Node_1 -->|10%| Exit
    Node_1 -->|60%| Node_2
    Node_1 -->|30%| Node_3
    Node_2 -->|60%| Exit
    Node_2 -->|10%| Node_1
    Node_2 -->|30%| Node_3
    Node_3 -->|20%| Exit
    Node_3 -->|40%| Node_2
    Node_3 -->|40%| Node_1

model_instance.save_diagram("urgentcare_resources.mmd", include_resources=True)

flowchart TD
    Arrivals_Node_1("Time between arrivals<br/>Exponential(λ=1.0)")
    Arrivals_Node_2("Time between arrivals<br/>Exponential(λ=4.0)")
    Arrivals_Node_3("Time between arrivals<br/>Exponential(λ=3.0)")
    Node_1["Node 1<br/>Lognormal(mean=1.0, stdev=0)"]
    Node_2["Node 2<br/>Normal(mean=1.0, sd=0)"]
    Node_3["Node 3<br/>Lognormal(mean=1.0, stdev=0)"]
    Resource_Resource_1(("Resource 1<br/>(18)"))
    Resource_Resource_2(("Resource 2<br/>(12)"))
    Resource_Resource_3(("Resource 3<br/>(25)"))
    Exit(["Exit"])

    Arrivals_Node_1 --> Node_1
    Arrivals_Node_2 --> Node_2
    Arrivals_Node_3 --> Node_3
    Resource_Resource_1 -.Seize.-> Node_1
    Node_1 -.Release.-> Resource_Resource_1
    Resource_Resource_2 -.Seize.-> Node_2
    Node_2 -.Release.-> Resource_Resource_2
    Resource_Resource_3 -.Seize.-> Node_3
    Node_3 -.Release.-> Resource_Resource_3
    Node_1 -->|10%| Exit
    Node_1 -->|60%| Node_2
    Node_1 -->|30%| Node_3
    Node_2 -->|60%| Exit
    Node_2 -->|10%| Node_1
    Node_2 -->|30%| Node_3
    Node_3 -->|20%| Exit
    Node_3 -->|40%| Node_2
    Node_3 -->|40%| Node_1

model_instance.get_distributions_df()
Activity Phase Distribution Type Parameters
0 Node 1 Arrival Exponential rate=1.0
1 Node 1 Service Lognormal mean=1.0, std=0.1
2 Node 2 Arrival Exponential rate=4.0
3 Node 2 Service Normal mean=1.0, std=0.1
4 Node 3 Arrival Exponential rate=3.0
5 Node 3 Service Lognormal mean=1.0, std=0.1
model_instance.get_routing_matrix_df()
Node 1 Node 2 Node 3 Exit
Source Activity
Node 1 0.0 0.6 0.3 0.1
Node 2 0.1 0.0 0.3 0.6
Node 3 0.4 0.4 0.0 0.2
model_instance.get_resources_df()
Resource Activity Count
0 Resource 1 Node 1 18
1 Resource 2 Node 2 12
2 Resource 3 Node 3 25

Convert to ciw parameters

adapter = CiwConverter(model_instance)
network_params = adapter.generate_params()
print(network_params)
{
    'number_of_servers': [18, 12, 25],
    'arrival_distributions': [Exponential(rate=1.0), Exponential(rate=4.0), Exponential(rate=3.0)],
    'service_distributions': [
        Lognormal(mean=-0.0049751654265839124, sd=0.0997513451195916),
        Normal(mean=1.0, sd=0.1),
        Lognormal(mean=-0.0049751654265839124, sd=0.0997513451195916)
    ],
    '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]]
}

Build and run the ciw model

network = ciw.create_network(**network_params)
sim = ciw.Simulation(network)
sim.simulate_until_max_time(50)
print("Quick simulation run worked!")
Quick simulation run worked!

Run the model for multiple replications

df_reps = multiple_replications(
    network,
    model_instance,
    num_reps=5,
    runtime=2880,
    warmup=1440,
    n_jobs=-1,
)

df_reps.head()
rep node_id activity_name resource_name resource_capacity n_service mean_wait mean_service utilisation mean_Lq
0 0 1 Node 1 Resource 1 18 7216 0.000000 1.000161 28.238367 0.000000
1 0 2 Node 2 Resource 2 12 14546 0.138830 1.000103 83.861598 1.402375
2 0 3 Node 3 Resource 3 25 10838 0.000000 0.999720 30.372911 0.000000
3 1 1 Node 1 Resource 1 18 7365 0.000000 1.001236 27.770561 0.000000
4 1 2 Node 2 Resource 2 12 14515 0.182148 1.000512 83.455292 1.836026

Convert to wide format

wide = tidy_to_wide_format(df_reps)
wide.head()
mean_Lq [Node 1] mean_Lq [Node 2] mean_Lq [Node 3] mean_service [Node 1] mean_service [Node 2] mean_service [Node 3] mean_wait [Node 1] mean_wait [Node 2] mean_wait [Node 3] n_service [Node 1] n_service [Node 2] n_service [Node 3] utilisation [Node 1] utilisation [Node 2] utilisation [Node 3]
rep
0 0.0 1.402375 0.0 1.000161 1.000103 0.999720 0.0 0.138830 0.0 7216 14546 10838 28.238367 83.861598 30.372911
1 0.0 1.836026 0.0 1.001236 1.000512 1.001197 0.0 0.182148 0.0 7365 14515 10841 27.770561 83.455292 29.982137
2 0.0 1.806245 0.0 1.000565 1.000158 0.999756 0.0 0.178762 0.0 7210 14550 10678 27.542854 83.531804 29.792481
3 0.0 1.291956 0.0 0.998776 0.999506 1.001227 0.0 0.129872 0.0 7160 14325 10713 27.765163 83.631392 29.968283
4 0.0 1.483820 0.0 0.999830 1.000044 1.000370 0.0 0.148063 0.0 7056 14431 10793 27.990271 83.738731 30.265584

Summarise results

summary = summarise_results(df_reps)
summary.round(1)
activity Metric Node 1 (Resource 1) Node 2 (Resource 2) Node 3 (Resource 3)
0 Mean completed services 7201.4 14473.4 10772.6
1 Mean waiting time 0.0 0.2 0.0
2 Mean service time 1.0 1.0 1.0
3 Mean utilisation 27.9 83.6 30.1
4 Mean queue length 0.0 1.6 0.0