Skip to content

Commit a46df9c

Browse files
committed
feat(mathematical): wrote R version of page
1 parent e603927 commit a46df9c

File tree

3 files changed

+595
-76
lines changed

3 files changed

+595
-76
lines changed

pages/verification_validation/mathematical.qmd

Lines changed: 183 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: Mathematical proof of correctness
2+
title: Mathematical proof of correctness
33
---
44

55
{{< include ../../scripts/_reticulate-setup.md >}}
@@ -14,15 +14,15 @@ title: ❌ Mathematical proof of correctness
1414

1515
**Learning objectives:**
1616

17-
* TODO
17+
* Understand why and how to verify a model using mathematical proof of correctness.
1818

1919
**Pre-reading:**
2020

2121
This page implements mathematical proof of correctness implemented on the [verification and validation page](verification_validation.qmd). It does so within a test ([tests](tests.qmd) page).
2222

2323
The test is run on the model from the [parallel processing](../output_analysis/parallel.qmd) page (likewise, also used on the [scenario and sensitivity analysis](../experiments/scenarios.qmd) page and [tests](tests.qmd) pages).
2424

25-
> *Entity generation → Entity processing → Initialisation bias → Performance measures → Replications → Length of warm-up → Number of replications → Parallel processing → Mathematical proof of correctness*
25+
> *Entity generation → Entity processing → Initialisation bias → Performance measures → Replications → Parallel processing → Mathematical proof of correctness*
2626
2727
**Required packages:**
2828

@@ -65,6 +65,7 @@ library(future.apply)
6565
library(ggplot2)
6666
library(knitr)
6767
library(plotly)
68+
library(queueing)
6869
library(simmer)
6970
library(tidyr)
7071
```
@@ -81,15 +82,9 @@ A mathematical model is a set of equations or formulas that describe how systems
8182

8283
Real-world systems are often too complex for neat equations (e.g., variable arrivals, different service rules or other complex features). In these cases, discrete-event simulation allows us to better mimic the system. However, if your simulation is simple enough - or if you start with a basic model to verify your code (and later build in complexity) - you can use mathematical proof of correctness.
8384

84-
The small example we've been building throughout this book is an instance of an **M/M/s queue model** (as is the nurse visit simulation example). For a reminder on what an M/M/s queue model is, see the section "*Find out more about M/M/s models*" on the [example conceptual models](../intro/examples.qmd) page. In the next section, we'll show how to verify our small example by comparing it to a mathematical model.
85+
The small example we've been building throughout this book is an instance of an **M/M/s queue model** (as is the nurse visit simulation example). For a reminder on what an M/M/s queue model is, see the section "*Find out more about M/M/s models*" on the [example conceptual models](../intro/examples.qmd) page. In the following sections, we'll show how to verify our small example by comparing it to a mathematical model.
8586

86-
## How to verify the model against mathematical results
87-
88-
::: {.python-content}
89-
90-
<div class="h3-tight"></div>
91-
92-
### Simulation code
87+
## Simulation code
9388

9489
The model used is the same as that on the [parallel processing](../output_analysis/parallel.qmd) page - as also used on the [scenario and sensitivity analysis](../experiments/scenarios.qmd) and [tests](tests.qmd) pages.
9590

@@ -98,7 +93,15 @@ The model used is the same as that on the [parallel processing](../output_analys
9893
#| echo: false
9994
```
10095

101-
### Mathematical M/M/s queue model
96+
```{r}
97+
#| file: "tests_resources/simulation.R"
98+
#| echo: false
99+
```
100+
101+
102+
## Mathematical M/M/s queue model
103+
104+
::: {.python-content}
102105

103106
The class implementing a mathematical M/M/s queue model is explained line-by-line below.
104107

@@ -238,7 +241,7 @@ class MMSQueue:
238241
return 1 / (sum_part + server_term)
239242
```
240243

241-
#### Explaining `MMSQueue`
244+
### Explaining `MMSQueue`
242245

243246
```{.python}
244247
class MMSQueue:
@@ -426,7 +429,53 @@ First, we compute P0 which is the probability that the system is completely empt
426429
"""
427430
# Sum for n = 0 to s-1
428431
sum_part = sum(
429-
(self.lambda_over_mu**n) / math.factorial(n)
432+
(self.lambda_over_mu**n) / math.factorial(n)# Validates a discrete event simulation of a healthcare M/M/S queue by
433+
# comparing simulation results to analytical queueing theory.
434+
#
435+
# Metrics (using standard queueing theory notation):
436+
# - ρ (rho): utilisation
437+
# - Lq: mean queue length
438+
# - W: mean time in system
439+
# - Wq: mean waiting
440+
#
441+
# Results must match theory with a 15% tolerance (accomodates stochasticity).
442+
# Tests are run across diverse parameter combinations and utilisation levels.
443+
# System stability requires arrival rate < number_of_servers * service_rate.
444+
445+
446+
#' Run simulation and return key performance indicators using standard queueing
447+
#' theory notation.
448+
#'
449+
#' The warm-up period should be sufficiently long to allow the system to reach
450+
#' steady-state before data collection begins.
451+
452+
run_simulation_model <- function(
453+
patient_inter, mean_n_consult_time, number_of_nurses
454+
) {
455+
# Run simulation
456+
param <- parameters(
457+
patient_inter = patient_inter,
458+
mean_n_consult_time = mean_n_consult_time,
459+
number_of_nurses = number_of_nurses,
460+
warm_up_period = 500L,
461+
data_collection_period = 1500L,
462+
number_of_runs = 100L,
463+
scenario_name = 0L,
464+
cores = 1L
465+
)
466+
run_results <- runner(param)[["run_results"]]
467+
468+
# Get overall results, using queueing theory notation in column names
469+
results <- run_results |>
470+
summarise(
471+
RO = mean(.data[["utilisation_nurse"]]),
472+
Lq = mean(.data[["mean_queue_length_nurse"]]),
473+
W = mean(.data[["mean_time_in_system"]]),
474+
Wq = mean(.data[["mean_waiting_time_nurse"]])
475+
)
476+
results
477+
}
478+
430479
for n in range(self.num_servers)
431480
)
432481
@@ -440,14 +489,53 @@ First, we compute P0 which is the probability that the system is completely empt
440489

441490
This method calculates P0, which is the probability that the system is completely empty - i.e., no customers are waiting and all servers are idle.
442491

443-
### Function to run the model
492+
:::
493+
494+
::: {.r-content}
495+
496+
In R, there is a package [queueing](https://cran.r-project.org/web/packages/queueing/index.html) which makes this task super easy!
497+
498+
To create a theoretical M/M/s queue model, we first need to calculate lambda and mu based on our inter-arrival time and service times:
499+
500+
```{r}
501+
# Example parameters
502+
interarrival_time <- 5
503+
service_length <- 5
504+
servers <- 2
505+
506+
# Calculate lambda and mu
507+
lambda <- 1L / interarrival_time
508+
mu <- 1L / service_length
509+
```
510+
511+
We can then use `queueing::NewInput.MMC()` to set-up an M/M/s (M/M/c) model. This function takes the arrival rate (`lambda`), service rate (`mu`) and the number of servers (`c`). There are two other parameters `n` and `method`, but we just specify the defaults here (0) (see [documentation](https://cran.r-project.org/web/packages/queueing/queueing.pdf) for more details).
512+
513+
```{r}
514+
math_model <- queueing::NewInput.MMC(
515+
lambda = lambda, mu = mu, c = servers, n = 0L, method = 0L
516+
)
517+
math_model
518+
```
519+
520+
We then provide this model to `queueing::QueueingModel()`, which builds the model and returns the relevant calculations.
521+
522+
```{r}
523+
math_results <- queueing::QueueingModel(math_model)
524+
math_results
525+
```
526+
527+
:::
528+
529+
## Function to run our simulation
444530

445531
This function simply runs our simulation model and returns the relevant metrics, renamed to use queueing theory notation.
446532

447533
We could have included this code as part of the test function below - but have just made it in a separate function to keep that test function a little simpler.
448534

449535
Our short illustrative parameters (30 minute warm-up, 40 minute data collection, 5 replications) are very unstable, so we have increased them to get more reliable results for the comparison in this test.
450536

537+
::: {.python-content}
538+
451539
```{python}
452540
def run_simulation_model(
453541
interarrival_time,
@@ -504,7 +592,62 @@ run_simulation_model(interarrival_time=5,
504592
number_of_doctors=3)
505593
```
506594

507-
### Verification test
595+
:::
596+
597+
::: {.r-content}
598+
599+
```{r}
600+
#' Run simulation and return key performance indicators using standard queueing
601+
#' theory notation.
602+
#'
603+
#' The warm-up period should be sufficiently long to allow the system to reach
604+
#' steady-state before data collection begins.
605+
#'
606+
#' @param interarrival_time Mean time between arrivals (minutes).
607+
#' @param consultation_time Mean length of doctor's consultation (minutes).
608+
#' @param number_of_doctors Number of doctors.
609+
610+
run_simulation_model <- function(
611+
interarrival_time, consultation_time, number_of_doctors
612+
) {
613+
# Run simulation
614+
param <- create_params(
615+
interarrival_time = interarrival_time,
616+
consultation_time = consultation_time,
617+
number_of_doctors = number_of_doctors,
618+
warm_up_period = 2000L,
619+
data_collection_period = 4000L,
620+
number_of_runs = 20L,
621+
verbose = FALSE,
622+
cores = 1L
623+
)
624+
run_results <- runner(param)[["run_results"]]
625+
626+
# Get overall results, using queueing theory notation in column names
627+
results <- run_results |>
628+
summarise(
629+
RO = mean(.data[["utilisation_doctor"]]),
630+
Lq = mean(.data[["mean_queue_length_doctor"]]),
631+
W = mean(.data[["mean_time_in_system"]]),
632+
Wq = mean(.data[["mean_wait_time_doctor"]])
633+
)
634+
results
635+
}
636+
```
637+
638+
For example:
639+
640+
```{r}
641+
run_simulation_model(interarrival_time = 5L,
642+
consultation_time = 10L,
643+
number_of_doctors = 3L)
644+
```
645+
646+
:::
647+
648+
## Test comparing simulation with mathematical model results
649+
650+
::: {.python-content}
508651

509652
Our test function `test_simulation_against_theory()` uses `@pytest.mark.parametrize` to run several different test cases.
510653

@@ -572,6 +715,29 @@ pytest.main(["tests_resources/test_mms.py"])
572715

573716
:::
574717

718+
::: {.r-content}
719+
720+
Our test function uses `patrick` to run several different test cases.
721+
722+
For each of these, it compares the results from the simulation model (from `run_simulation_model()`) with the results from the mathematical model from `queueing`, and checks they are reasonably similar.
723+
724+
```{r}
725+
#| file: "tests_resources/test_mms.R"
726+
#| eval: false
727+
```
728+
729+
```{r}
730+
#| echo: false
731+
#| output: false
732+
library(testthat)
733+
```
734+
735+
```{r}
736+
#| echo: false
737+
testthat::test_file(file.path("tests_resources", "test_mms.R"))
738+
```
739+
740+
:::
575741

576742
## Explore the example models
577743

@@ -599,8 +765,7 @@ pytest.main(["tests_resources/test_mms.py"])
599765
::: {.blue-table}
600766
| | |
601767
| - | - |
602-
| **Key files** | |
603-
| **What to look for?** | |
768+
| **Key files** | `tests/testthat/test-mms.R` |
604769
: {tbl-colwidths="[20,80]"}
605770
:::
606771

0 commit comments

Comments
 (0)