Skip to content

Reference

Orchestrator for running multiple simulation iterations (runs) and aggregating results.

The Trial class manages the execution of multiple Model instances as defined in the global configuration. It collects performance metrics, financial data, and patient-level logs from each individual run into centralised DataFrames for cross-run analysis.

Attributes:

Name Type Description
df_trial_results DataFrame

A summary DataFrame where each row represents a single simulation run. Tracks metrics such as mean queue times, occupancy, and financial savings.

model_objects list

A collection of Model instances created during the trial, allowing for post-hoc inspection of specific run states.

trial_patient_dataframes list

A list of DataFrames, each containing detailed attribute data for every patient in a specific run.

trial_patient_df DataFrame

The master DataFrame created by concatenating all patient-level data across all runs in the trial.

trial_info str

A descriptive string containing the configuration settings used for the current trial (e.g., SDEC therapy status and resource availability).

Notes

GENAI declaration (SR): this docstring has been generated with the aid of Google Gemini Flash. All generated content has been thoroughly reviewed.

Source code in src/stroke_ward_model/trial.py
class Trial:
    """
    Orchestrator for running multiple simulation iterations (runs) and
    aggregating results.

    The Trial class manages the execution of multiple `Model` instances as
    defined in the global configuration. It collects performance metrics,
    financial data, and patient-level logs from each individual run into
    centralised DataFrames for cross-run analysis.

    Attributes
    ----------
    df_trial_results : pd.DataFrame
        A summary DataFrame where each row represents a single simulation run.
        Tracks metrics such as mean queue times, occupancy, and financial
        savings.
    model_objects : list
        A collection of `Model` instances created during the trial, allowing
        for post-hoc inspection of specific run states.
    trial_patient_dataframes : list
        A list of DataFrames, each containing detailed attribute data for every
        patient in a specific run.
    trial_patient_df : pd.DataFrame
        The master DataFrame created by concatenating all patient-level data
        across all runs in the trial.
    trial_info : str
        A descriptive string containing the configuration settings used for
        the current trial (e.g., SDEC therapy status and resource availability).

    Notes
    -----
    GENAI declaration (SR): this docstring has been generated with the aid
    of Google Gemini Flash.
    All generated content has been thoroughly reviewed.
    """

    # The constructor sets up a pandas dataframe that will store the key
    # results from each run with run number as the index.

    def __init__(self):
        self.df_trial_results = pd.DataFrame()
        self.df_trial_results["Run Number"] = [0]
        self.df_trial_results["Mean Q Time Nurse (Mins)"] = [0.0]
        self.df_trial_results["Max Q Time Nurse (Mins)"] = [0.0]
        self.df_trial_results["Number of Admissions Avoided In Run"] = [0.0]
        self.df_trial_results["Mean Q Time Ward (Hour)"] = [0.0]
        self.df_trial_results["Max Q Time Ward (Hour)"] = [0.0]
        self.df_trial_results["Mean Occupancy"] = [0.0]
        self.df_trial_results["Number of Admission Delays"] = [0.0]
        self.df_trial_results["Mean Length of Stay Ward (Hours)"] = [0.0]
        self.df_trial_results["Financial Savings of Admissions Avoidance (£)"] = [0.0]
        self.df_trial_results["SDEC Medical Staff Cost (£)"] = [0.0]
        self.df_trial_results["SDEC Savings (£)"] = [0.0]
        self.df_trial_results["Thrombolysis Savings (£)"] = [0.0]
        self.df_trial_results["Total Savings"] = [0.0]
        self.df_trial_results["Mean MRS Change"] = [0.0]
        self.df_trial_results["Mean Number of Patients Assessed"] = [0.0]
        self.df_trial_results["Number of Intracranial Haemhorrhage patients"] = [0.0]
        self.df_trial_results["Number of Ischaemic Stroke patients"] = [0.0]
        self.df_trial_results["Number of TIA patients"] = [0.0]
        self.df_trial_results["Number of Stroke Mimic patients"] = [0.0]
        self.df_trial_results["Number of Non-Stroke patients"] = [0.0]
        self.df_trial_results[
            "Mean Additional Thrombolysed Patients From CTP Running"
        ] = [0.0]
        self.df_trial_results.set_index("Run Number", inplace=True)

        self.ward_occupancy_audits = []
        self.ward_occupancy_df = pd.DataFrame()

        self.sdec_occupancy_audits = []
        self.sdec_occupancy_df = pd.DataFrame()

        self.model_objects = []
        # self.patient_objects = {}

        self.trial_patient_dataframes = []
        self.trial_patient_df = pd.DataFrame()

        self.results_dataframes = []
        self.trial_results_df = pd.DataFrame()

    # MARK: M: run_trial
    # Method to run a trial

    def run_trial(self):
        """
        Executes the batch of simulation runs and aggregates the resulting data.

        This method performs the following steps:

        1. Loops through the number of runs specified in `g.number_of_runs`.

        2. Instantiates and executes a `Model` for each run.

        3. Collects summary metrics (e.g., queue times, savings) into `df_trial_results`.

        4. Flattens patient-level data into a single master DataFrame.

        5. Calculates trial-level means and updates the global `g` class attributes.

        6. Optionally exports results to a CSV file if `g.write_to_csv` is True.

        This method dynamically updates the global configuration class `g` by
        calculating the mean of results across all runs and storing them in
        dictionaries keyed by the trial counter.

        See Also
        --------
        Model.run : The method called to execute an individual simulation iteration.

        Notes
        -----
        GENAI declaration (SR): this docstring has been generated with the aid
        of Google Gemini Flash.
        All generated content has been thoroughly reviewed.
        """
        # Run the simulation for the number of runs specified in g class.
        # For each run, we create a new instance of the Model class and call its
        # run method, which sets everything else in motion.  Once the run has
        # completed, we grab out the stored run results
        # and store it against the run number in the trial results dataframe.

        for run in range(g.number_of_runs):
            my_model = Model(run)
            my_model.run()

            self.model_objects.append(my_model)

            self.df_trial_results.loc[run] = [
                my_model.mean_q_time_nurse,
                my_model.max_q_time_nurse,
                my_model.number_of_admissions_avoided,
                my_model.mean_q_time_ward,
                my_model.max_q_time_ward,
                my_model.mean_ward_occupancy,
                my_model.admission_delays,
                my_model.mean_los_ward,
                my_model.sdec_financial_savings,
                my_model.medical_staff_cost,
                my_model.savings_sdec,
                my_model.thrombolysis_savings,
                my_model.total_savings,
                my_model.mean_mrs_change,
                my_model.patient_counter,
                my_model.ich_patients_count,
                my_model.i_patients_count,
                my_model.tia_patients_count,
                my_model.stroke_mimic_patient_count,
                my_model.non_stroke_patient_count,
                my_model.additional_thrombolysis_from_ctp,
            ]

            # self.patient_objects[run] = my_model.patient_objects
            patient_dataframe = pd.DataFrame(
                [p.__dict__ for p in my_model.patient_objects]
            )
            patient_dataframe["run"] = run + 1
            self.trial_patient_dataframes.append(patient_dataframe)
            my_model.results_df["run"] = run + 1
            self.results_dataframes.append(my_model.results_df)

            my_model.ward_occupancy_graph_df["run"] = run + 1
            self.ward_occupancy_audits.append(my_model.ward_occupancy_graph_df)

            my_model.sdec_occupancy_graph_df["run"] = run + 1
            self.sdec_occupancy_audits.append(my_model.sdec_occupancy_graph_df)

        self.trial_patient_df = pd.concat(self.trial_patient_dataframes)
        self.ward_occupancy_df = pd.concat(self.ward_occupancy_audits)
        self.sdec_occupancy_df = pd.concat(self.sdec_occupancy_audits)
        self.trial_results_df = pd.concat(self.results_dataframes)

        if g.write_to_csv == True:
            self.df_trial_results.to_csv(
                f"trial {g.trials_run_counter} trial results.csv", index=False
            )

        # TODO: SR: FIX appending of per-run graphs to trial class
        # if g.gen_graph:
        #     self.graph_objects.append(my_model.plot_stroke_run_graphs(plot=False))

        # This is new code that will store all averages to compare across
        # the different trials. It does this by checking if the attribute
        # exists in the global g class, and if it doesn't it creates it. It
        # then stores the mean of each run against the attribute
        # (eg "trial_mean_q_time_nurse")

        # The mean is stored against the key of g.trials_run_counter.

        for attr, col in [
            ("trial_mean_q_time_nurse", "Mean Q Time Nurse (Mins)"),
            ("trial_max_q_time_nurse", "Max Q Time Nurse (Mins)"),
            (
                "trial_number_of_admissions_avoided",
                "Number of Admissions Avoided In Run",
            ),
            ("trial_mean_q_time_ward", "Mean Q Time Ward (Hour)"),
            ("trial_max_q_time_ward", "Max Q Time Ward (Hour)"),
            ("trial_mean_occupancy", "Mean Occupancy"),
            ("trial_number_of_admission_delays", "Number of Admission Delays"),
            (
                "trial_financial_savings_of_a_a",
                "Financial Savings of Admissions Avoidance (£)",
            ),
            ("sdec_medical_cost", "SDEC Medical Staff Cost (£)"),
            ("trial_sdec_financial_savings", "SDEC Savings (£)"),
            ("trial_thrombolysis_savings", "Thrombolysis Savings (£)"),
            ("trial_total_savings", "Total Savings"),
            ("trial_mrs_change", "Mean MRS Change"),
            ("trial_patient_count", "Mean Number of Patients Assessed"),
            (
                "trial_additional_thrombolysis_from_ctp",
                "Mean Additional Thrombolysed Patients From CTP Running",
            ),
        ]:
            # Checks to see if the attribute already exists and if it doesn't
            # create it. Creates a mean of each trial and creates a dictionary
            # that can be read later.

            if not hasattr(g, attr):
                setattr(g, attr, {})
            if "max" in attr:
                getattr(g, attr)[g.trials_run_counter] = round(
                    self.df_trial_results[col].max(), 2
                )
            else:
                getattr(g, attr)[g.trials_run_counter] = round(
                    self.df_trial_results[col].mean(), 2
                )

        # Code to store the configuration that was used for this trial.
        self.trial_info = (
            f"Trial {g.trials_run_counter}, SDEC Therapy = {g.therapy_sdec},"
            f" SDEC Open % = {g.sdec_value}, CTP Open % = {g.ctp_value}"
        )

        print("---------------------------------------------------")
        print(f"{self.trial_info}")
        print(f"Trial {g.trials_run_counter} Results:")
        print(" ")
        print(
            f"Trial Mean Q Time Nurse (Mins):     \
              {g.trial_mean_q_time_nurse[g.trials_run_counter]}"
        )
        print(
            f"Trial Max Q Time Nurse (Mins):     \
              {g.trial_max_q_time_nurse[g.trials_run_counter]}"
        )
        print(
            f"Trial Number of Admissions Avoided: \
              {g.trial_number_of_admissions_avoided[g.trials_run_counter]}"
        )
        print(
            f"Trial Mean Q Time Ward (Hours):     \
              {g.trial_mean_q_time_ward[g.trials_run_counter]}"
        )
        print(
            f"Trial Max Q Time Ward (Hours):     \
              {g.trial_max_q_time_ward[g.trials_run_counter]}"
        )
        print(
            f"Trial Mean Ward Occupancy:          \
              {g.trial_mean_occupancy[g.trials_run_counter]}"
        )
        print(
            f"Trial Number of Admission Delays:   \
              {g.trial_number_of_admission_delays[g.trials_run_counter]}"
        )
        print(
            f"Trial SDEC Total Savings (£):       \
              {g.trial_financial_savings_of_a_a[g.trials_run_counter]}"
        )
        print(
            f"Trial SDEC Medical Cost (£):        \
              {g.sdec_medical_cost[g.trials_run_counter]}"
        )
        print(
            f"Trial SDEC Savings - Cost (£):      \
              {g.trial_sdec_financial_savings[g.trials_run_counter]}"
        )
        print(
            f"Trial Thrombolysis Savings (£):     \
              {g.trial_thrombolysis_savings[g.trials_run_counter]}"
        )
        print(
            f"Trial Total Savings (£):            \
              {g.trial_total_savings[g.trials_run_counter]}"
        )
        print(
            f"Mean MRS Change:                    \
              {g.trial_mrs_change[g.trials_run_counter]}"
        )
        print(
            f"Mean Assessed Patients:                    \
              {g.trial_patient_count[g.trials_run_counter]}"
        )
        print(
            f"Mean Additional Thrombolysed Patients From CTP Running:                    \
              {g.trial_additional_thrombolysis_from_ctp[g.trials_run_counter]}"
        )

run_trial()

Executes the batch of simulation runs and aggregates the resulting data.

This method performs the following steps:

  1. Loops through the number of runs specified in g.number_of_runs.

  2. Instantiates and executes a Model for each run.

  3. Collects summary metrics (e.g., queue times, savings) into df_trial_results.

  4. Flattens patient-level data into a single master DataFrame.

  5. Calculates trial-level means and updates the global g class attributes.

  6. Optionally exports results to a CSV file if g.write_to_csv is True.

This method dynamically updates the global configuration class g by calculating the mean of results across all runs and storing them in dictionaries keyed by the trial counter.

See Also

Model.run : The method called to execute an individual simulation iteration.

Notes

GENAI declaration (SR): this docstring has been generated with the aid of Google Gemini Flash. All generated content has been thoroughly reviewed.

Source code in src/stroke_ward_model/trial.py
def run_trial(self):
    """
    Executes the batch of simulation runs and aggregates the resulting data.

    This method performs the following steps:

    1. Loops through the number of runs specified in `g.number_of_runs`.

    2. Instantiates and executes a `Model` for each run.

    3. Collects summary metrics (e.g., queue times, savings) into `df_trial_results`.

    4. Flattens patient-level data into a single master DataFrame.

    5. Calculates trial-level means and updates the global `g` class attributes.

    6. Optionally exports results to a CSV file if `g.write_to_csv` is True.

    This method dynamically updates the global configuration class `g` by
    calculating the mean of results across all runs and storing them in
    dictionaries keyed by the trial counter.

    See Also
    --------
    Model.run : The method called to execute an individual simulation iteration.

    Notes
    -----
    GENAI declaration (SR): this docstring has been generated with the aid
    of Google Gemini Flash.
    All generated content has been thoroughly reviewed.
    """
    # Run the simulation for the number of runs specified in g class.
    # For each run, we create a new instance of the Model class and call its
    # run method, which sets everything else in motion.  Once the run has
    # completed, we grab out the stored run results
    # and store it against the run number in the trial results dataframe.

    for run in range(g.number_of_runs):
        my_model = Model(run)
        my_model.run()

        self.model_objects.append(my_model)

        self.df_trial_results.loc[run] = [
            my_model.mean_q_time_nurse,
            my_model.max_q_time_nurse,
            my_model.number_of_admissions_avoided,
            my_model.mean_q_time_ward,
            my_model.max_q_time_ward,
            my_model.mean_ward_occupancy,
            my_model.admission_delays,
            my_model.mean_los_ward,
            my_model.sdec_financial_savings,
            my_model.medical_staff_cost,
            my_model.savings_sdec,
            my_model.thrombolysis_savings,
            my_model.total_savings,
            my_model.mean_mrs_change,
            my_model.patient_counter,
            my_model.ich_patients_count,
            my_model.i_patients_count,
            my_model.tia_patients_count,
            my_model.stroke_mimic_patient_count,
            my_model.non_stroke_patient_count,
            my_model.additional_thrombolysis_from_ctp,
        ]

        # self.patient_objects[run] = my_model.patient_objects
        patient_dataframe = pd.DataFrame(
            [p.__dict__ for p in my_model.patient_objects]
        )
        patient_dataframe["run"] = run + 1
        self.trial_patient_dataframes.append(patient_dataframe)
        my_model.results_df["run"] = run + 1
        self.results_dataframes.append(my_model.results_df)

        my_model.ward_occupancy_graph_df["run"] = run + 1
        self.ward_occupancy_audits.append(my_model.ward_occupancy_graph_df)

        my_model.sdec_occupancy_graph_df["run"] = run + 1
        self.sdec_occupancy_audits.append(my_model.sdec_occupancy_graph_df)

    self.trial_patient_df = pd.concat(self.trial_patient_dataframes)
    self.ward_occupancy_df = pd.concat(self.ward_occupancy_audits)
    self.sdec_occupancy_df = pd.concat(self.sdec_occupancy_audits)
    self.trial_results_df = pd.concat(self.results_dataframes)

    if g.write_to_csv == True:
        self.df_trial_results.to_csv(
            f"trial {g.trials_run_counter} trial results.csv", index=False
        )

    # TODO: SR: FIX appending of per-run graphs to trial class
    # if g.gen_graph:
    #     self.graph_objects.append(my_model.plot_stroke_run_graphs(plot=False))

    # This is new code that will store all averages to compare across
    # the different trials. It does this by checking if the attribute
    # exists in the global g class, and if it doesn't it creates it. It
    # then stores the mean of each run against the attribute
    # (eg "trial_mean_q_time_nurse")

    # The mean is stored against the key of g.trials_run_counter.

    for attr, col in [
        ("trial_mean_q_time_nurse", "Mean Q Time Nurse (Mins)"),
        ("trial_max_q_time_nurse", "Max Q Time Nurse (Mins)"),
        (
            "trial_number_of_admissions_avoided",
            "Number of Admissions Avoided In Run",
        ),
        ("trial_mean_q_time_ward", "Mean Q Time Ward (Hour)"),
        ("trial_max_q_time_ward", "Max Q Time Ward (Hour)"),
        ("trial_mean_occupancy", "Mean Occupancy"),
        ("trial_number_of_admission_delays", "Number of Admission Delays"),
        (
            "trial_financial_savings_of_a_a",
            "Financial Savings of Admissions Avoidance (£)",
        ),
        ("sdec_medical_cost", "SDEC Medical Staff Cost (£)"),
        ("trial_sdec_financial_savings", "SDEC Savings (£)"),
        ("trial_thrombolysis_savings", "Thrombolysis Savings (£)"),
        ("trial_total_savings", "Total Savings"),
        ("trial_mrs_change", "Mean MRS Change"),
        ("trial_patient_count", "Mean Number of Patients Assessed"),
        (
            "trial_additional_thrombolysis_from_ctp",
            "Mean Additional Thrombolysed Patients From CTP Running",
        ),
    ]:
        # Checks to see if the attribute already exists and if it doesn't
        # create it. Creates a mean of each trial and creates a dictionary
        # that can be read later.

        if not hasattr(g, attr):
            setattr(g, attr, {})
        if "max" in attr:
            getattr(g, attr)[g.trials_run_counter] = round(
                self.df_trial_results[col].max(), 2
            )
        else:
            getattr(g, attr)[g.trials_run_counter] = round(
                self.df_trial_results[col].mean(), 2
            )

    # Code to store the configuration that was used for this trial.
    self.trial_info = (
        f"Trial {g.trials_run_counter}, SDEC Therapy = {g.therapy_sdec},"
        f" SDEC Open % = {g.sdec_value}, CTP Open % = {g.ctp_value}"
    )

    print("---------------------------------------------------")
    print(f"{self.trial_info}")
    print(f"Trial {g.trials_run_counter} Results:")
    print(" ")
    print(
        f"Trial Mean Q Time Nurse (Mins):     \
          {g.trial_mean_q_time_nurse[g.trials_run_counter]}"
    )
    print(
        f"Trial Max Q Time Nurse (Mins):     \
          {g.trial_max_q_time_nurse[g.trials_run_counter]}"
    )
    print(
        f"Trial Number of Admissions Avoided: \
          {g.trial_number_of_admissions_avoided[g.trials_run_counter]}"
    )
    print(
        f"Trial Mean Q Time Ward (Hours):     \
          {g.trial_mean_q_time_ward[g.trials_run_counter]}"
    )
    print(
        f"Trial Max Q Time Ward (Hours):     \
          {g.trial_max_q_time_ward[g.trials_run_counter]}"
    )
    print(
        f"Trial Mean Ward Occupancy:          \
          {g.trial_mean_occupancy[g.trials_run_counter]}"
    )
    print(
        f"Trial Number of Admission Delays:   \
          {g.trial_number_of_admission_delays[g.trials_run_counter]}"
    )
    print(
        f"Trial SDEC Total Savings (£):       \
          {g.trial_financial_savings_of_a_a[g.trials_run_counter]}"
    )
    print(
        f"Trial SDEC Medical Cost (£):        \
          {g.sdec_medical_cost[g.trials_run_counter]}"
    )
    print(
        f"Trial SDEC Savings - Cost (£):      \
          {g.trial_sdec_financial_savings[g.trials_run_counter]}"
    )
    print(
        f"Trial Thrombolysis Savings (£):     \
          {g.trial_thrombolysis_savings[g.trials_run_counter]}"
    )
    print(
        f"Trial Total Savings (£):            \
          {g.trial_total_savings[g.trials_run_counter]}"
    )
    print(
        f"Mean MRS Change:                    \
          {g.trial_mrs_change[g.trials_run_counter]}"
    )
    print(
        f"Mean Assessed Patients:                    \
          {g.trial_patient_count[g.trials_run_counter]}"
    )
    print(
        f"Mean Additional Thrombolysed Patients From CTP Running:                    \
          {g.trial_additional_thrombolysis_from_ctp[g.trials_run_counter]}"
    )