Пример #1
0
def test_params_to_from_array():
    params = Params()
    params_array = params.asarray()

    params_from_array = Params.fromarray(params_array)
    params_from_array_array = params_from_array.asarray()

    assert np.all(params_array == params_from_array_array)
Пример #2
0
def test_more_overweight_become_symptomatic():
    snapshot = Snapshot.random(nplaces, npeople, nslots)

    people_statuses_test_data = np.full(npeople, DiseaseStatus.Exposed.value, dtype=np.uint32)
    people_transition_times_test_data = np.full(npeople, 1, dtype=np.uint32)
    # set all people to obesity=2, corresponding to overweight
    people_obesity_test_data = np.full(npeople, 2, dtype=np.uint8)
    people_transition_times_test_data = np.full(npeople, 1, dtype=np.uint32)
    people_ages_test_data = np.full(npeople, 18, dtype=np.uint16)

    snapshot.buffers.people_statuses[:] = people_statuses_test_data
    snapshot.buffers.people_obesity[:] = people_obesity_test_data
    snapshot.buffers.people_transition_times[:] = people_transition_times_test_data
    snapshot.buffers.people_ages[:] = people_ages_test_data

    params = Params()
    base_proportion_asymptomatic = 0.79
    params.proportion_asymptomatic = base_proportion_asymptomatic
    overweight_sympt_mplier = 1.46
    params.overweight_sympt_mplier = overweight_sympt_mplier
    snapshot.update_params(params)

    simulator = Simulator(snapshot, gpu=False)
    simulator.upload_all(snapshot.buffers)

    people_statuses_before = np.zeros(npeople, dtype=np.uint32)
    simulator.download("people_statuses", people_statuses_before)

    assert np.array_equal(people_statuses_before, people_statuses_test_data)

    simulator.step_kernel("people_update_statuses")

    # check that statuses don't change after one step
    people_statuses_after_one_step = np.zeros(npeople, dtype=np.uint32)
    simulator.download("people_statuses", people_statuses_after_one_step)

    assert np.array_equal(people_statuses_after_one_step, people_statuses_test_data)

    # run another timestep, this time statuses should change
    simulator.step_kernel("people_update_statuses")

    # assert that statuses change to either symptomatic or asymptomatic in the correct proportion
    people_statuses_after_two_steps = np.zeros(npeople, dtype=np.uint32)
    simulator.download("people_statuses", people_statuses_after_two_steps)

    num_asymptomatic = np.count_nonzero(people_statuses_after_two_steps == DiseaseStatus.Asymptomatic.value)
    result_proportion_asymptomatic = num_asymptomatic / npeople

    num_presymptomatic = np.count_nonzero(people_statuses_after_two_steps == DiseaseStatus.Presymptomatic.value)
    result_proportion_presymptomatic = num_presymptomatic / npeople

    base_proportion_symptomatic = 1 - base_proportion_asymptomatic
    expected_proportion_presymptomatic = overweight_sympt_mplier * base_proportion_symptomatic
    expected_proportion_asymptomatic = 1 - expected_proportion_presymptomatic

    assert np.isclose(expected_proportion_asymptomatic, result_proportion_asymptomatic, atol=0.01)
    assert np.isclose(expected_proportion_presymptomatic, result_proportion_presymptomatic, atol=0.01)
def test_diabetes_higher_mortality():
    snapshot = Snapshot.random(nplaces, npeople, nslots)

    people_ages_test_data = np.full(npeople, 65, dtype=np.uint16)
    people_statuses_test_data = np.full(npeople,
                                        DiseaseStatus.Symptomatic.value,
                                        dtype=np.uint32)
    # set all people to diabetes=1
    people_diabetes_test_data = np.ones(npeople, dtype=np.uint8)
    people_transition_times_test_data = np.full(npeople, 1, dtype=np.uint32)

    snapshot.buffers.people_ages[:] = people_ages_test_data
    snapshot.buffers.people_statuses[:] = people_statuses_test_data
    snapshot.buffers.people_diabetes[:] = people_diabetes_test_data
    snapshot.buffers.people_transition_times[:] = people_transition_times_test_data

    params = Params()
    diabetes_multiplier = 1.4
    params.diabetes_multiplier = diabetes_multiplier
    snapshot.update_params(params)

    simulator = Simulator(snapshot, gpu=False)
    simulator.upload_all(snapshot.buffers)

    people_statuses_before = np.zeros(npeople, dtype=np.uint32)
    simulator.download("people_statuses", people_statuses_before)

    assert np.array_equal(people_statuses_before, people_statuses_test_data)

    # run two timesteps so statuses should change
    simulator.step_kernel("people_update_statuses")
    simulator.step_kernel("people_update_statuses")

    # assert that statuses change to either recovered or dead in the correct proportion
    people_statuses_after_two_steps = np.zeros(npeople, dtype=np.uint32)
    simulator.download("people_statuses", people_statuses_after_two_steps)

    num_recovered = np.count_nonzero(
        people_statuses_after_two_steps == DiseaseStatus.Recovered.value)
    proportion_recovered = num_recovered / npeople

    num_dead = np.count_nonzero(
        people_statuses_after_two_steps == DiseaseStatus.Dead.value)
    proportion_dead = num_dead / npeople

    # expected recovery probability for ages 60-70
    expected_proportion_dead = 0.0193
    expected_proportion_dead *= diabetes_multiplier
    expected_proportion_recovered = 1 - expected_proportion_dead

    assert np.isclose(expected_proportion_recovered,
                      proportion_recovered,
                      atol=0.01)
    assert np.isclose(expected_proportion_dead, proportion_dead, atol=0.01)
Пример #4
0
def test_correct_send_hazard():
    # Set up and upload the test data
    snapshot = Snapshot.random(nplaces, npeople, nslots)

    people_place_ids = np.full((npeople, nslots),
                               sentinel_value,
                               dtype=np.uint32)
    people_place_ids[0][0:4] = [0, 5, 7, 3]
    people_place_ids[1][0:4] = [1, 5, 6, 4]
    people_place_ids[2][0:4] = [2, 3, 7, 6]

    people_flows = np.zeros((npeople, nslots), dtype=np.float32)
    people_flows[0][0:4] = [0.8, 0.1, 0.06, 0.04]
    people_flows[1][0:4] = [0.7, 0.18, 0.09, 0.03]
    people_flows[2][0:4] = [0.6, 0.2, 0.16, 0.04]

    people_statuses = np.full(npeople,
                              DiseaseStatus.Symptomatic.value,
                              dtype=np.uint32)

    snapshot.buffers.people_flows[:] = people_flows.flatten()
    snapshot.buffers.people_place_ids[:] = people_place_ids.flatten()
    snapshot.buffers.people_statuses[:] = people_statuses
    snapshot.buffers.place_activities[:] = np.random.randint(4,
                                                             size=nplaces,
                                                             dtype=np.uint32)

    params = Params()
    params.place_hazard_multipliers = np.ones(5, dtype=np.float32)
    snapshot.update_params(params)

    simulator = Simulator(snapshot, gpu=False)
    simulator.upload_all(snapshot.buffers)

    # Run the kernel
    simulator.step_kernel("people_send_hazards")

    # Download the result
    place_hazards = np.zeros(nplaces, dtype=np.uint32)
    simulator.download("place_hazards", place_hazards)
    place_counts = np.zeros(nplaces, dtype=np.uint32)
    simulator.download("place_counts", place_counts)

    # Assert expected results
    expected_place_hazards_floats = np.array(
        [0.8, 0.7, 0.6, 0.24, 0.03, 0.28, 0.13, 0.22], dtype=np.float32)
    expected_place_hazards = (fixed_factor *
                              expected_place_hazards_floats).astype(np.uint32)
    expected_place_counts = np.array([1, 1, 1, 2, 1, 2, 2, 2], dtype=np.uint32)

    assert np.allclose(expected_place_hazards, place_hazards)
    assert np.array_equal(expected_place_counts, place_counts)
Пример #5
0
    def zeros(cls, nplaces, npeople, nslots):
        """Creates a snapshot with correctly sized but zeroed arrays."""
        nplaces = np.uint32(nplaces)
        npeople = np.uint32(npeople)
        nslots = np.uint32(nslots)
        time = np.uint32(0)
        area_codes = np.full(npeople, "E02004129")
        not_home_probs = np.zeros(npeople).astype(np.float32)

        lockdown_multipliers = np.ones(100)

        buffers = Buffers(
            place_activities=np.zeros(nplaces, dtype=np.uint32),
            place_coords=np.zeros(nplaces * 2, dtype=np.float32),
            place_hazards=np.zeros(nplaces, dtype=np.uint32),
            place_counts=np.zeros(nplaces, dtype=np.uint32),
            people_ages=np.zeros(npeople, dtype=np.uint16),
            people_obesity=np.zeros(npeople, dtype=np.uint16),
            people_cvd=np.zeros(npeople, dtype=np.uint8),
            people_diabetes=np.zeros(npeople, dtype=np.uint8),
            people_blood_pressure=np.zeros(npeople, dtype=np.uint8),
            people_statuses=np.zeros(npeople, dtype=np.uint32),
            people_transition_times=np.zeros(npeople, dtype=np.uint32),
            people_place_ids=np.zeros(npeople * nslots, dtype=np.uint32),
            people_baseline_flows=np.zeros(npeople * nslots, dtype=np.float32),
            people_flows=np.zeros(npeople * nslots, dtype=np.float32),
            people_hazards=np.zeros(npeople, dtype=np.float32),
            people_prngs=np.zeros(npeople * 4, dtype=np.uint32),
            params=Params().asarray(),
        )

        return cls(nplaces, npeople, nslots, time, area_codes, not_home_probs,
                   lockdown_multipliers, buffers)
Пример #6
0
    def random(cls, nplaces, npeople, nslots, lat=50.7, lon=-3.5):
        """Generates a random snapshot for testing in a 1 degree square around lat/lon."""
        nplaces = np.uint32(nplaces)
        npeople = np.uint32(npeople)
        nslots = np.uint32(nslots)
        time = np.uint32(0)
        area_codes = np.full(npeople, "E02004129")
        not_home_probs = np.random.rand(npeople).astype(np.float32)

        lockdown_multipliers = np.ones(100)

        buffers = Buffers(
            place_activities=np.random.randint(5,
                                               size=nplaces,
                                               dtype=np.uint32),
            place_coords=np.random.randn(nplaces * 2).astype(np.float32),
            place_hazards=np.zeros(nplaces, dtype=np.uint32),
            place_counts=np.zeros(nplaces, dtype=np.uint32),
            people_ages=np.random.randint(100, size=npeople, dtype=np.uint16),
            people_obesity=np.zeros(npeople, dtype=np.uint16),
            people_cvd=np.zeros(npeople, dtype=np.uint8),
            people_diabetes=np.zeros(npeople, dtype=np.uint8),
            people_blood_pressure=np.zeros(npeople, dtype=np.uint8),
            people_statuses=np.random.binomial(1, 0.001,
                                               npeople).astype(np.uint32),
            people_transition_times=np.ones(npeople, dtype=np.uint32),
            people_place_ids=np.random.randint(nplaces,
                                               size=npeople * nslots,
                                               dtype=np.uint32),
            people_baseline_flows=np.random.rand(npeople * nslots).astype(
                np.float32),
            people_flows=np.zeros(npeople * nslots, dtype=np.float32),
            people_hazards=np.zeros(npeople, dtype=np.float32),
            people_prngs=np.random.randint(np.uint32((1 << 32) - 1),
                                           size=npeople * 4,
                                           dtype=np.uint32),
            params=Params().asarray(),
        )

        ids = np.reshape(np.tile(np.arange(nslots), npeople),
                         (npeople, nslots))
        ids += np.reshape(np.arange(npeople), (npeople, 1))
        buffers.people_place_ids[:] = np.clip(
            ids, 0, nplaces).flatten().astype(np.uint32)

        buffers.place_coords[0::2] = np.mod(
            np.arange(nplaces), np.sqrt(nplaces)) / np.sqrt(nplaces)
        buffers.place_coords[1::2] = np.arange(nplaces) / nplaces
        buffers.place_coords[0::2] += lat - 0.5
        buffers.place_coords[1::2] += lon - 0.5
        buffers.place_coords[:] += np.random.randn(2 * nplaces) / 100.0

        return cls(nplaces, npeople, nslots, time, area_codes, not_home_probs,
                   lockdown_multipliers, buffers)
Пример #7
0
def run_headless(simulator,
                 snapshot,
                 iterations,
                 quiet,
                 store_detailed_counts=True):
    """
    Run the simulation in headless mode and store summary data.
    NB: running in this mode is required in order to view output data in the dashboard. Also store_detailed_counts must
    be set to True to output the required data for the dashboard, however the model runs faster with this set to False.
    """
    params = Params.fromarray(snapshot.buffers.params)
    summary = Summary(snapshot,
                      store_detailed_counts=store_detailed_counts,
                      max_time=iterations)

    # only show progress bar in quiet mode
    timestep_iterator = range(iterations) if quiet else tqdm(
        range(iterations), desc="Running simulation")

    for time in timestep_iterator:
        # Update parameters based on lockdown
        params.set_lockdown_multiplier(snapshot.lockdown_multipliers, time)
        simulator.upload("params", params.asarray())

        # Step the simulator
        simulator.step()

        # Update the statuses
        simulator.download("people_statuses", snapshot.buffers.people_statuses)
        summary.update(time, snapshot.buffers.people_statuses)

    if not quiet:
        for i in range(iterations):
            print(f"\nDay {i}")
            summary.print_counts(i)

    if not quiet:
        print("\nFinished")

    # Download the snapshot from OpenCL to host memory
    final_state = simulator.download_all(snapshot.buffers)

    return summary, final_state
Пример #8
0
    def from_arrays(cls, people_ages, people_obesity, people_cvd,
                    people_diabetes, people_blood_pressure, people_place_ids,
                    people_baseline_flows, area_codes, not_home_probs,
                    place_activities, place_coords, lockdown_multipliers):
        nplaces = place_activities.shape[0]
        npeople = people_place_ids.shape[0]
        nslots = people_baseline_flows.shape[1]

        # Reshape 2D arrays to 1D
        people_place_ids = people_place_ids.flatten()
        people_baseline_flows = people_baseline_flows.flatten()
        place_coords = place_coords.flatten()

        nplaces = np.uint32(nplaces)
        npeople = np.uint32(npeople)
        nslots = np.uint32(nslots)
        time = np.uint32(0)

        buffers = Buffers(
            place_activities=place_activities,
            place_coords=place_coords,
            place_hazards=np.zeros(nplaces, dtype=np.uint32),
            place_counts=np.zeros(nplaces, dtype=np.uint32),
            people_ages=people_ages,
            people_obesity=people_obesity,
            people_cvd=people_cvd,
            people_diabetes=people_diabetes,
            people_blood_pressure=people_blood_pressure,
            people_statuses=np.zeros(npeople, dtype=np.uint32),
            people_transition_times=np.zeros(npeople, dtype=np.uint32),
            people_place_ids=people_place_ids,
            people_baseline_flows=people_baseline_flows,
            people_flows=people_baseline_flows,
            people_hazards=np.zeros(npeople, dtype=np.float32),
            people_prngs=np.random.randint(np.uint32((1 << 32) - 1),
                                           size=npeople * 4,
                                           dtype=np.uint32),
            params=Params().asarray(),
        )

        return cls(nplaces, npeople, nslots, time, area_codes, not_home_probs,
                   lockdown_multipliers, buffers)
Пример #9
0
def create_params(calibration_params, disease_params):
    current_risk_beta = disease_params["current_risk_beta"]

    # NB: OpenCL model incorporates the current risk beta by pre-multiplying the hazard multipliers with it
    location_hazard_multipliers = LocationHazardMultipliers(
        retail=calibration_params["hazard_location_multipliers"]["Retail"] *
        current_risk_beta,
        primary_school=calibration_params["hazard_location_multipliers"]
        ["PrimarySchool"] * current_risk_beta,
        secondary_school=calibration_params["hazard_location_multipliers"]
        ["SecondarySchool"] * current_risk_beta,
        home=calibration_params["hazard_location_multipliers"]["Home"] *
        current_risk_beta,
        work=calibration_params["hazard_location_multipliers"]["Work"] *
        current_risk_beta,
    )

    individual_hazard_multipliers = IndividualHazardMultipliers(
        presymptomatic=calibration_params["hazard_individual_multipliers"]
        ["presymptomatic"],
        asymptomatic=calibration_params["hazard_individual_multipliers"]
        ["asymptomatic"],
        symptomatic=calibration_params["hazard_individual_multipliers"]
        ["symptomatic"])

    obesity_multipliers = [
        disease_params["overweight"], disease_params["obesity_30"],
        disease_params["obesity_35"], disease_params["obesity_40"]
    ]

    return Params(
        location_hazard_multipliers=location_hazard_multipliers,
        individual_hazard_multipliers=individual_hazard_multipliers,
        obesity_multipliers=obesity_multipliers,
        cvd_multiplier=disease_params["cvd"],
        diabetes_multiplier=disease_params["diabetes"],
        bloodpressure_multiplier=disease_params["bloodpressure"],
    )
Пример #10
0
    def create_parameters(parameters_file: str = None,
                          current_risk_beta: float = None,
                          infection_log_scale: float = None,
                          infection_mode: float = None,
                          presymptomatic: float = None,
                          asymptomatic: float = None,
                          symptomatic: float = None,
                          retail: float = None,
                          primary_school: float = None,
                          secondary_school: float = None,
                          home: float = None,
                          work: float = None,
                          ):
        """Create a params object with the given arguments. This replicates the functionality in
        microsim.main.create_params() but rather than just reading parameters from the parameters
        json file, it allows some of the parameters to be set manually.

        Also note that some (constant) parameters can be set by calling the `set_constants` method.
        This is useful for cases where parameters should override the defaults specified in the
        parameters file but cannot be called directly by the function that is running the model"""

        # Read the default parameters from a yml file, then override with any provided
        if parameters_file is None:
            parameters_file = os.path.join(".", "model_parameters", "default.yml")
        elif not os.path.isfile(parameters_file):
            raise Exception(f"The given parameters file '{parameters_file} is not a file.")

        with open(parameters_file) as f:
            parameters = yaml.load(f, Loader=yaml.SafeLoader)

        sim_params = parameters["microsim"]  # Parameters for the dynamic microsim (python)
        calibration_params = parameters["microsim_calibration"]
        disease_params = parameters["disease"]  # Parameters for the disease model (r)

        # current_risk_beta needs to be set first  as the OpenCL model pre-multiplies the hazard multipliers by it
        current_risk_beta = OpenCLRunner._check_if_none("current_risk_beta", current_risk_beta, disease_params['current_risk_beta'])

        # Location hazard multipliers can be passed straight through to the LocationHazardMultipliers object.
        # If no argument was passed then the default in the parameters file is used. Note that they need to
        # be multiplied by the current_risk_beta
        location_hazard_multipliers = LocationHazardMultipliers(
            retail=current_risk_beta * OpenCLRunner._check_if_none("retail",
                retail, calibration_params["hazard_location_multipliers"]["Retail"]),
            primary_school=current_risk_beta * OpenCLRunner._check_if_none("primary_school",
                primary_school, calibration_params["hazard_location_multipliers"]["PrimarySchool"]),
            secondary_school=current_risk_beta * OpenCLRunner._check_if_none("secondary_school",
                secondary_school, calibration_params["hazard_location_multipliers"]["SecondarySchool"]),
            home=current_risk_beta * OpenCLRunner._check_if_none("home",
                home, calibration_params["hazard_location_multipliers"]["Home"]),
            work=current_risk_beta * OpenCLRunner._check_if_none("work",
                work, calibration_params["hazard_location_multipliers"]["Work"]),
        )

        # Individual hazard multipliers can be passed straight through
        individual_hazard_multipliers = IndividualHazardMultipliers(
            presymptomatic=OpenCLRunner._check_if_none("presymptomatic",
                presymptomatic, calibration_params["hazard_individual_multipliers"]["presymptomatic"]),
            asymptomatic=OpenCLRunner._check_if_none("asymptomatic",
                asymptomatic, calibration_params["hazard_individual_multipliers"]["asymptomatic"]),
            symptomatic=OpenCLRunner._check_if_none("symptomatic",
                symptomatic, calibration_params["hazard_individual_multipliers"]["symptomatic"])
        )

        # Some parameters are set in the default.yml file and can be overridden
        pass  # None here yet
        
        obesity_multipliers = np.array([disease_params["overweight"], disease_params["obesity_30"],disease_params["obesity_35"], disease_params["obesity_40"]])

        cvd = disease_params["cvd"]
        diabetes = disease_params["diabetes"]
        bloodpressure = disease_params["bloodpressure"]
        overweight_sympt_mplier = disease_params["overweight_sympt_mplier"]

        p = Params(
            location_hazard_multipliers=location_hazard_multipliers,
            individual_hazard_multipliers=individual_hazard_multipliers,
        )

        # Remaining parameters are defined within the Params class and have to be manually overridden
        if infection_log_scale is not None:
            p.infection_log_scale = infection_log_scale
        if infection_mode is not None:
            p.infection_mode = infection_mode
            
        p.obesity_multipliers = obesity_multipliers
        p.cvd_multiplier = cvd
        p.diabetes_multiplier = diabetes
        p.bloodpressure_multiplier = bloodpressure
        p.overweight_sympt_mplier = overweight_sympt_mplier
        
        return p
Пример #11
0
    def __init__(self,
                 simulator,
                 snapshot,
                 nlines,
                 window_name,
                 width,
                 height,
                 font_path="microsim/opencl/fonts/RobotoMono.ttf"):
        """Create the window, imgui renderer, and all background renderers.

        Args:
            nplaces: Number of places being simulated.
            npeople: Number of people being simulated.
            nlines: Number of connection lines to draw per person (recommend low, must be < nslots).
            window_name: The name to display on the application window.
            width: Initial width of the window in screen coordinates.
            height: Initial height of the window in screen coordinates.
            font_path: Path the the .ttf file to use for text in imgui.
        """
        nplaces = simulator.nplaces
        npeople = simulator.npeople
        device = simulator.device_name()
        platform = simulator.platform_name()

        if not glfw.init():
            raise OSError("Could not initialize window")

        glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
        glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
        glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
        glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, glfw.TRUE)

        window = glfw.create_window(width, height, window_name, None, None)
        if not window:
            glfw.terminate()
            raise OSError("Could not initialize window")

        glfw.make_context_current(window)
        imgui.create_context()
        impl = GlfwRenderer(window)

        glfw.set_framebuffer_size_callback(window, self.resize_callback)
        glfw.set_key_callback(window, self.key_callback)

        font = imgui.get_io().fonts.add_font_from_file_ttf(font_path, 56)
        impl.refresh_font_texture()

        # vertices representing corners of the screen
        quad_vertices = np.array([
            -1.0,
            -1.0,
            1.0,
            1.0,
            -1.0,
            1.0,
            -1.0,
            -1.0,
            1.0,
            1.0,
            1.0,
            -1.0,
        ],
                                 dtype=np.float32)

        # Create vertex buffers on the GPU
        quad_vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, quad_vbo)
        glBufferData(GL_ARRAY_BUFFER, 4 * 2 * 6, quad_vertices, GL_STATIC_DRAW)

        locations_vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, locations_vbo)
        glBufferData(GL_ARRAY_BUFFER, 4 * 2 * nplaces, None, GL_STATIC_DRAW)

        hazards_vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, hazards_vbo)
        glBufferData(GL_ARRAY_BUFFER, 4 * nplaces, None, GL_DYNAMIC_DRAW)

        links_ebo = glGenBuffers(1)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, links_ebo)
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * 2 * npeople * nlines, None,
                     GL_STATIC_DRAW)

        # Set up the vao for the point shader
        point_vao = glGenVertexArrays(1)
        glBindVertexArray(point_vao)
        glBindBuffer(GL_ARRAY_BUFFER, locations_vbo)
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, None)
        glEnableVertexAttribArray(0)
        glBindBuffer(GL_ARRAY_BUFFER, hazards_vbo)
        glVertexAttribIPointer(1, 1, GL_UNSIGNED_INT, 4, None)
        glEnableVertexAttribArray(1)

        # Set up the vao for the line shader
        line_vao = glGenVertexArrays(1)
        glBindVertexArray(line_vao)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, links_ebo)
        glBindBuffer(GL_ARRAY_BUFFER, locations_vbo)
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, None)
        glEnableVertexAttribArray(0)
        glBindBuffer(GL_ARRAY_BUFFER, hazards_vbo)
        glVertexAttribIPointer(1, 1, GL_UNSIGNED_INT, 4, None)
        glEnableVertexAttribArray(1)

        # Set up the vao for the quad
        quad_vao = glGenVertexArrays(1)
        glBindVertexArray(quad_vao)
        glBindBuffer(GL_ARRAY_BUFFER, quad_vbo)
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, None)
        glEnableVertexAttribArray(0)

        glBindVertexArray(0)

        # Load and compile shaders
        places_program = load_shader("places")
        grid_program = load_shader("grid")

        # Enable OpenGL features
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

        # Initialise Camera position
        position = np.array([0.0, 0.0, 0.05], dtype=np.float32)

        # Imgui styling
        style = imgui.get_style()
        set_styles(style)

        # Make a guess on font size
        font_scale = 0.5
        self.update_font_scale(font_scale)

        # Initialise viewport based on framebuffer
        width, height = glfw.get_framebuffer_size(window)
        glViewport(0, 0, width, height)

        self.simulator = simulator
        self.snapshot = snapshot
        self.initial_state_snapshot = copy.deepcopy(snapshot)
        self.params = Params.fromarray(snapshot.buffers.params)
        self.nplaces = nplaces
        self.npeople = npeople
        self.nlines = nlines
        self.width = width
        self.height = height
        self.platform = platform
        self.device = device
        self.window = window
        self.first = True
        self.impl = impl
        self.font = font
        self.font_scale = font_scale

        self.simulation_active = False
        self.do_lockdown = False
        self.point_size = 2.0
        self.show_grid = True
        self.show_points = True
        self.show_lines = False
        self.show_parameters = False
        self.show_saveas = False
        self.spacing = 40.0
        self.move_sensitivity = 10.0
        self.zoom_multiplier = 1.01
        self.position = position
        self.snapshot_dir = "microsim/opencl/snapshots"
        self.snapshots = [
            f for f in os.listdir(self.snapshot_dir) if f.endswith(".npz")
        ]
        self.current_snapshot = self.snapshots.index(f"{snapshot.name}.npz")
        self.selected_snapshot = self.current_snapshot
        self.saveas_file = self.snapshots[self.current_snapshot]
        self.summary = Summary(snapshot, store_detailed_counts=False)

        self.quad_vbo = quad_vbo
        self.locations_vbo = locations_vbo
        self.hazards_vbo = hazards_vbo
        self.links_ebo = links_ebo
        self.point_vao = point_vao
        self.line_vao = line_vao
        self.quad_vao = quad_vao
        self.places_program = places_program
        self.grid_program = grid_program

        self.upload_hazards(self.snapshot.buffers.place_hazards)
        self.upload_locations(self.snapshot.buffers.place_coords)
        self.upload_links(self.snapshot.buffers.people_place_ids)
Пример #12
0
def test_infection_transition_times_distribution(visualize=False):
    npeople = 1000000
    snapshot = Snapshot.random(nplaces, npeople, nslots)

    test_hazard = 0.9

    people_hazards_test_data = np.full(npeople, test_hazard, dtype=np.float32)
    people_statuses_test_data = np.full(npeople, DiseaseStatus.Presymptomatic.value, dtype=np.uint32)
    people_transition_times_test_data = np.zeros(npeople, dtype=np.uint32)

    snapshot.buffers.people_hazards[:] = people_hazards_test_data
    snapshot.buffers.people_statuses[:] = people_statuses_test_data
    snapshot.buffers.people_transition_times[:] = people_transition_times_test_data

    params = Params()
    infection_log_scale = 0.75
    infection_mode = 7.0
    params.infection_log_scale = infection_log_scale
    params.infection_mode = infection_mode
    snapshot.update_params(params)

    simulator = Simulator(snapshot, gpu=False)
    simulator.upload_all(snapshot.buffers)

    simulator.step_kernel("people_update_statuses")

    people_statuses_after = np.zeros(npeople, dtype=np.uint32)
    simulator.download("people_statuses", people_statuses_after)

    people_transition_times_after = np.zeros(npeople, dtype=np.uint32)
    simulator.download("people_transition_times", people_transition_times_after)

    # Check that transition times are distributed with a log-normal distribution
    adjusted_transition_times = people_transition_times_after + 1
    mean = adjusted_transition_times.mean()
    std_dev = adjusted_transition_times.std()
    mode = scipy.stats.mode(adjusted_transition_times)[0][0]

    meanlog = infection_log_scale**2 + np.log(infection_mode)
    expected_samples = np.random.lognormal(mean=meanlog, sigma=infection_log_scale, size=npeople)
    # round samples to nearest integer
    expected_samples = np.rint(expected_samples)
    expected_mean = expected_samples.mean()
    expected_std_dev = expected_samples.std()
    expected_mode = scipy.stats.mode(expected_samples)[0][0]

    # Float to integer rounding and clamping at zero makes the original random numbers hard
    # to recover so we have slightly larger tolerances here to avoid false negatives.
    assert np.isclose(expected_mean, mean, atol=0.7)
    assert np.isclose(expected_std_dev, std_dev, atol=0.4)
    assert np.isclose(expected_mode, mode, atol=1.0)

    # check that mode is similar to original mode parameter
    assert np.isclose(infection_mode, mode, atol=1.0)

    if visualize:  # show histogram of distribution
        fig, ax = plt.subplots(1, 1)
        ax.hist(adjusted_transition_times, bins=50, range=[0, 60])
        plt.title("Result Samples")
        plt.show()
        fig, ax = plt.subplots(1, 1)
        ax.hist(expected_samples, bins=50, range=[0, 60])
        plt.title("Expected Samples")
        plt.show()
Пример #13
0
    def run(self):

        # If this is the first data assimilation window, we can just run the model as normal
        if self.start_day == 0:
            assert self.current_particle_pop_df is None  # Shouldn't have any preivously-created particles
            # load snapshot
            snapshot = Snapshot.load_full_snapshot(path=self.snapshot_file)
            # set params
            snapshot.update_params(self.params)
            # Can set the random seed to make it deterministic (None means np will choose one randomly)
            snapshot.seed_prngs(seed=None)

            # Create a simulator and upload the snapshot data to the OpenCL device
            simulator = Simulator(snapshot,
                                  opencl_dir=self.opencl_dir,
                                  gpu=self.use_gpu)
            simulator.upload_all(snapshot.buffers)

            if not self.quiet:
                # print(f"Running simulation {sim_number + 1}.")
                print(f"Running simulation")

            params = Params.fromarray(
                snapshot.buffers.params
            )  # XX Why extract Params? Can't just use PARAMS?

            summary = Summary(
                snapshot,
                store_detailed_counts=self.store_detailed_counts,
                max_time=self.run_length  # Total length of the simulation
            )

            # only show progress bar in quiet mode
            timestep_iterator = range(self.run_length) if self.quiet \
                else tqdm(range(self.quiet), desc="Running simulation")

            iter_count = 0  # Count the total number of iterations
            # Run for iterations days
            for _ in timestep_iterator:
                # Update parameters based on lockdown
                params.set_lockdown_multiplier(snapshot.lockdown_multipliers,
                                               iter_count)
                simulator.upload("params", params.asarray())

                # Step the simulator
                simulator.step()
                iter_count += 1

            # Update the statuses
            simulator.download("people_statuses",
                               snapshot.buffers.people_statuses)
            summary.update(iter_count, snapshot.buffers.people_statuses)

            if not self.quiet:
                for i in range(self.run_length):
                    print(f"\nDay {i}")
                    summary.print_counts(i)

            if not self.quiet:
                print("\nFinished")

            # Download the snapshot from OpenCL to host memory
            # XX This is 'None'.
            final_state = simulator.download_all(snapshot.buffers)

            pass
        else:  # Otherwise we need to restart previous models stored in the current_particle_pop_df
            # XXXX CAN GET OLD MODEL STATES, WITH ALL DISEASE STATUSES, FROM THE DF. TWO ISSUES
            # 1. But need to work out how to draw these appropriately; can't assume they are each as good as
            # each other. THIS SHOULD BE OK, surely there's a way to go from the final particles and weights
            # to the DF of state vectors. Particle ID? Just try it out.
            # 2. Also: what to do about stochasticity. For a given (global) parameter combination, we will
            # get quite different results depending on the mode state. - I DON'T THINK THIS IS A PROBLEM.
            # ABC Commonly used with stochastic models. E.g. https://eprints.lancs.ac.uk/id/eprint/80439/1/mainR1.pdf
            #
            raise Exception("Not implemented yet")

        # Return the current state of the model in a dictionary describing what it is
        #return {"simulator": simulator}
        return {"simulator": snapshot}
Пример #14
0
    def draw_parameters_window(self):
        """UI window with sliders for changing parameter values."""
        imgui.begin("Parameter Editor")

        imgui.text("Behaviour Change")

        _, self.params.symptomatic_multiplier = imgui.slider_float(
            "Symptomatic Multiplier", self.params.symptomatic_multiplier, 0.0,
            1.0)

        imgui.text("Duration Distributions")

        _, self.params.exposed_scale = imgui.slider_float(
            "Exposed Weibull Scale", self.params.exposed_scale, 1.0, 10.0)
        _, self.params.exposed_shape = imgui.slider_float(
            "Exposed Weibull Shape", self.params.exposed_shape, 0.0, 10.0)
        _, self.params.presymptomatic_scale = imgui.slider_float(
            "Presymptomatic Weibull Scale", self.params.presymptomatic_scale,
            0.0, 10.0)
        _, self.params.presymptomatic_shape = imgui.slider_float(
            "Presymptomatic Weibull Shape", self.params.presymptomatic_shape,
            0.0, 10.0)
        _, self.params.infection_log_scale = imgui.slider_float(
            "Infection Log-normal Scale", self.params.infection_log_scale, 0.0,
            5.0)
        _, self.params.infection_mode = imgui.slider_float(
            "Infection Log-normal Mode", self.params.infection_mode, 0.0, 20.0)

        imgui.text("Activity Hazard Multipliers")

        for i, activity in enumerate(list(Activity)):
            _, self.params.place_hazard_multipliers[i] = imgui.slider_float(
                activity.name, self.params.place_hazard_multipliers[i], 0.0,
                1.0, "%.4f")

        imgui.text("Mortality Probabilities by Age")

        _, self.params.mortality_probs[0] = imgui.slider_float(
            "0 to 4", self.params.mortality_probs[0], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[1] = imgui.slider_float(
            "5 to 9", self.params.mortality_probs[1], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[2] = imgui.slider_float(
            "10 to 14", self.params.mortality_probs[2], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[3] = imgui.slider_float(
            "15 to 19", self.params.mortality_probs[3], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[4] = imgui.slider_float(
            "20 to 24", self.params.mortality_probs[4], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[5] = imgui.slider_float(
            "25 to 29", self.params.mortality_probs[5], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[6] = imgui.slider_float(
            "30 to 34", self.params.mortality_probs[6], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[7] = imgui.slider_float(
            "35 to 39", self.params.mortality_probs[7], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[8] = imgui.slider_float(
            "40 to 44", self.params.mortality_probs[8], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[9] = imgui.slider_float(
            "45 to 49", self.params.mortality_probs[9], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[10] = imgui.slider_float(
            "50 to 54", self.params.mortality_probs[10], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[11] = imgui.slider_float(
            "55 to 59", self.params.mortality_probs[11], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[12] = imgui.slider_float(
            "60 to 64", self.params.mortality_probs[12], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[13] = imgui.slider_float(
            "65 to 69", self.params.mortality_probs[13], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[14] = imgui.slider_float(
            "70 to 74", self.params.mortality_probs[14], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[15] = imgui.slider_float(
            "75 to 79", self.params.mortality_probs[15], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[16] = imgui.slider_float(
            "80 to 84", self.params.mortality_probs[16], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[17] = imgui.slider_float(
            "85 to 89", self.params.mortality_probs[17], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[18] = imgui.slider_float(
            "90 and above", self.params.mortality_probs[18], 0.0, 1.0, "%.7f")

        if imgui.button("Reset to Defaults"):
            self.params = Params()

        imgui.end()
Пример #15
0
def test_correct_flow_calculation_no_lockdown():
    snapshot = Snapshot.random(nplaces, npeople, nslots)

    people_place_ids_test_data = np.full((npeople, nslots),
                                         sentinel_value,
                                         dtype=np.uint32)
    people_place_ids_test_data[0][0:4] = [0, 5, 7, 3]
    people_place_ids_test_data[1][0:4] = [1, 5, 6, 4]
    people_place_ids_test_data[2][0:4] = [2, 3, 7, 6]

    people_flows_test_data = np.zeros((npeople, nslots), dtype=np.float32)
    people_flows_test_data[0][0:4] = [0.8, 0.1, 0.06, 0.04]
    people_flows_test_data[1][0:4] = [0.7, 0.18, 0.09, 0.03]
    people_flows_test_data[2][0:4] = [0.6, 0.2, 0.16, 0.04]

    people_statuses_test_data = np.full(npeople,
                                        DiseaseStatus.Susceptible.value,
                                        dtype=np.uint32)
    symptomatic_person_id = 1
    people_statuses_test_data[symptomatic_person_id] = np.uint32(
        DiseaseStatus.Symptomatic.value)

    place_activities_test_data = np.full(nplaces,
                                         Activity.Retail.value,
                                         dtype=np.uint32)
    place_activities_test_data[:3] = Activity.Home.value

    snapshot.buffers.people_baseline_flows[:] = people_flows_test_data.flatten(
    )
    snapshot.buffers.people_flows[:] = people_flows_test_data.flatten()
    snapshot.buffers.people_place_ids[:] = people_place_ids_test_data.flatten()
    snapshot.buffers.people_statuses[:] = people_statuses_test_data
    snapshot.buffers.place_activities[:] = place_activities_test_data

    params = Params()
    symptomatic_multiplier = 0.5
    params.symptomatic_multiplier = symptomatic_multiplier
    snapshot.update_params(params)

    simulator = Simulator(snapshot, gpu=False)
    simulator.upload_all(snapshot.buffers)

    people_flows_before = np.zeros(npeople * nslots, dtype=np.float32)
    simulator.download("people_flows", people_flows_before)

    expected_people_flows_before = people_flows_test_data.flatten()
    assert np.array_equal(people_flows_before, expected_people_flows_before)

    simulator.step_kernel("people_update_flows")

    people_flows_after = np.zeros(npeople * nslots, dtype=np.float32)
    simulator.download("people_flows", people_flows_after)

    # adjust symptomatic persons flows according to symptomatic multiplier
    expected_people_flows_after = people_flows_test_data
    expected_people_flows_after[symptomatic_person_id][0:4] = [
        0.85, 0.09, 0.045, 0.015
    ]
    expected_people_flows_after = expected_people_flows_after.flatten()

    assert not np.array_equal(people_flows_before, people_flows_after)
    assert np.array_equal(expected_people_flows_after, people_flows_after)
Пример #16
0
def test_asymptomatics_add_less_hazard():
    # Set up and upload the test data
    snapshot = Snapshot.random(nplaces, npeople, nslots)

    people_place_ids = np.full((npeople, nslots),
                               sentinel_value,
                               dtype=np.uint32)
    people_place_ids[0][0:4] = [0, 5, 7, 3]
    people_place_ids[1][0:4] = [1, 5, 6, 4]
    people_place_ids[2][0:4] = [2, 3, 7, 6]

    people_flows = np.zeros((npeople, nslots), dtype=np.float32)
    people_flows[0][0:4] = [0.8, 0.1, 0.06, 0.04]
    people_flows[1][0:4] = [0.7, 0.18, 0.09, 0.03]
    people_flows[2][0:4] = [0.6, 0.2, 0.16, 0.04]

    people_statuses = np.full(npeople,
                              DiseaseStatus.Symptomatic.value,
                              dtype=np.uint32)

    snapshot.buffers.people_flows[:] = people_flows.flatten()
    snapshot.buffers.people_place_ids[:] = people_place_ids.flatten()
    snapshot.buffers.people_statuses[:] = people_statuses
    snapshot.buffers.place_activities[:] = np.random.randint(4,
                                                             size=nplaces,
                                                             dtype=np.uint32)

    params = Params()
    params.place_hazard_multipliers = np.ones(5, dtype=np.float32)
    asymptomatic_multiplier = 0.5
    params.individual_hazard_multipliers = np.array(
        [1.0, asymptomatic_multiplier, 1.0])
    snapshot.update_params(params)

    simulator = Simulator(snapshot, gpu=False)
    simulator.upload_all(snapshot.buffers)

    # Run the kernel
    simulator.step_kernel("people_send_hazards")

    # Download the result
    place_hazards = np.zeros(nplaces, dtype=np.uint32)
    simulator.download("place_hazards", place_hazards)
    place_counts = np.zeros(nplaces, dtype=np.uint32)
    simulator.download("place_counts", place_counts)

    # Assert expected results
    expected_place_hazards_floats = np.array(
        [0.8, 0.7, 0.6, 0.24, 0.03, 0.28, 0.13, 0.22], dtype=np.float32)
    expected_place_hazards = (fixed_factor *
                              expected_place_hazards_floats).astype(np.uint32)

    assert np.allclose(expected_place_hazards, place_hazards)

    # Change statuses so all people are asymptomatic
    people_statuses_asymptomatic = np.full(npeople,
                                           DiseaseStatus.Asymptomatic.value,
                                           dtype=np.uint32)
    simulator.upload("people_statuses", people_statuses_asymptomatic)

    # reset place hazards to zero
    simulator.step_kernel("places_reset")

    # run kernel with asymptomatic population
    simulator.step_kernel("people_send_hazards")

    # Download the result
    place_hazards_asymptomatic = np.zeros(nplaces, dtype=np.uint32)
    simulator.download("place_hazards", place_hazards_asymptomatic)

    # Assert expected results
    expected_place_hazards_floats_asymptomatic = expected_place_hazards_floats * asymptomatic_multiplier
    expected_place_hazards_asymptomatic = (
        fixed_factor * expected_place_hazards_floats_asymptomatic).astype(
            np.uint32)

    assert np.allclose(expected_place_hazards_asymptomatic,
                       place_hazards_asymptomatic)
    assert place_hazards_asymptomatic.sum() < place_hazards.sum()
Пример #17
0
def test_correct_flow_calculation_with_lockdown():
    snapshot = Snapshot.random(nplaces, npeople, nslots)

    people_place_ids_test_data = np.full((npeople, nslots),
                                         sentinel_value,
                                         dtype=np.uint32)
    people_place_ids_test_data[0][0:4] = [0, 5, 7, 3]
    people_place_ids_test_data[1][0:4] = [1, 5, 6, 4]
    people_place_ids_test_data[2][0:4] = [2, 3, 7, 6]

    people_flows_test_data = np.zeros((npeople, nslots), dtype=np.float32)
    people_flows_test_data[0][0:4] = [0.8, 0.1, 0.06, 0.04]
    people_flows_test_data[1][0:4] = [0.7, 0.18, 0.09, 0.03]
    people_flows_test_data[2][0:4] = [0.6, 0.2, 0.16, 0.04]

    people_statuses_test_data = np.full(npeople,
                                        DiseaseStatus.Susceptible.value,
                                        dtype=np.uint32)
    symptomatic_person_id = 1
    people_statuses_test_data[symptomatic_person_id] = np.uint32(
        DiseaseStatus.Symptomatic.value)

    place_activities_test_data = np.full(nplaces,
                                         Activity.Retail.value,
                                         dtype=np.uint32)
    place_activities_test_data[:3] = Activity.Home.value

    snapshot.buffers.people_baseline_flows[:] = people_flows_test_data.flatten(
    )
    snapshot.buffers.people_flows[:] = people_flows_test_data.flatten()
    snapshot.buffers.people_place_ids[:] = people_place_ids_test_data.flatten()
    snapshot.buffers.people_statuses[:] = people_statuses_test_data
    snapshot.buffers.place_activities[:] = place_activities_test_data

    params = Params()
    params.set_lockdown_multiplier(lockdown_multipliers, 0)
    params.symptomatic_multiplier = 0.5
    snapshot.buffers.params[:] = params.asarray()

    simulator = Simulator(snapshot, gpu=False)
    simulator.upload_all(snapshot.buffers)

    people_flows_before = np.zeros(npeople * nslots, dtype=np.float32)
    simulator.download("people_flows", people_flows_before)

    expected_people_flows_before = people_flows_test_data.flatten()
    assert np.array_equal(people_flows_before, expected_people_flows_before)

    simulator.step_kernel("people_update_flows")

    people_flows_after = np.zeros(npeople * nslots, dtype=np.float32)
    simulator.download("people_flows", people_flows_after)

    assert not np.array_equal(people_flows_before, people_flows_after)

    # assert correct flows for symptomatic person
    expected_symptomatic_flows_after = np.array([0.85, 0.09, 0.045, 0.015])
    symptomatic_person_start_idx = symptomatic_person_id * nslots
    symptomatic_flows_after = people_flows_after[
        symptomatic_person_start_idx:symptomatic_person_start_idx + 4]

    assert np.allclose(expected_symptomatic_flows_after,
                       symptomatic_flows_after)

    # assert correct flows for person who is not symptomatic
    # adjustments calculated using first lockdown multiplier (approx. 0.9084687)
    expected_non_symptomatic_flows_after = np.array(
        [0.63661252, 0.18169374, 0.145354992, 0.036338748])
    non_symptomatic_person_id = 2
    person_start_idx = non_symptomatic_person_id * nslots
    non_symptomatic_flows_after = people_flows_after[
        person_start_idx:person_start_idx + 4]

    assert np.allclose(expected_non_symptomatic_flows_after,
                       non_symptomatic_flows_after)
Пример #18
0
class Inspector:
    """User Interface: manager for all user input and rendering for the application."""
    def __init__(self,
                 simulator,
                 snapshot,
                 nlines,
                 window_name,
                 width,
                 height,
                 font_path="microsim/opencl/fonts/RobotoMono.ttf"):
        """Create the window, imgui renderer, and all background renderers.

        Args:
            nplaces: Number of places being simulated.
            npeople: Number of people being simulated.
            nlines: Number of connection lines to draw per person (recommend low, must be < nslots).
            window_name: The name to display on the application window.
            width: Initial width of the window in screen coordinates.
            height: Initial height of the window in screen coordinates.
            font_path: Path the the .ttf file to use for text in imgui.
        """
        nplaces = simulator.nplaces
        npeople = simulator.npeople
        device = simulator.device_name()
        platform = simulator.platform_name()

        if not glfw.init():
            raise OSError("Could not initialize window")

        glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
        glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
        glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
        glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, glfw.TRUE)

        window = glfw.create_window(width, height, window_name, None, None)
        if not window:
            glfw.terminate()
            raise OSError("Could not initialize window")

        glfw.make_context_current(window)
        imgui.create_context()
        impl = GlfwRenderer(window)

        glfw.set_framebuffer_size_callback(window, self.resize_callback)
        glfw.set_key_callback(window, self.key_callback)

        font = imgui.get_io().fonts.add_font_from_file_ttf(font_path, 56)
        impl.refresh_font_texture()

        # vertices representing corners of the screen
        quad_vertices = np.array([
            -1.0,
            -1.0,
            1.0,
            1.0,
            -1.0,
            1.0,
            -1.0,
            -1.0,
            1.0,
            1.0,
            1.0,
            -1.0,
        ],
                                 dtype=np.float32)

        # Create vertex buffers on the GPU
        quad_vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, quad_vbo)
        glBufferData(GL_ARRAY_BUFFER, 4 * 2 * 6, quad_vertices, GL_STATIC_DRAW)

        locations_vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, locations_vbo)
        glBufferData(GL_ARRAY_BUFFER, 4 * 2 * nplaces, None, GL_STATIC_DRAW)

        hazards_vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, hazards_vbo)
        glBufferData(GL_ARRAY_BUFFER, 4 * nplaces, None, GL_DYNAMIC_DRAW)

        links_ebo = glGenBuffers(1)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, links_ebo)
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * 2 * npeople * nlines, None,
                     GL_STATIC_DRAW)

        # Set up the vao for the point shader
        point_vao = glGenVertexArrays(1)
        glBindVertexArray(point_vao)
        glBindBuffer(GL_ARRAY_BUFFER, locations_vbo)
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, None)
        glEnableVertexAttribArray(0)
        glBindBuffer(GL_ARRAY_BUFFER, hazards_vbo)
        glVertexAttribIPointer(1, 1, GL_UNSIGNED_INT, 4, None)
        glEnableVertexAttribArray(1)

        # Set up the vao for the line shader
        line_vao = glGenVertexArrays(1)
        glBindVertexArray(line_vao)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, links_ebo)
        glBindBuffer(GL_ARRAY_BUFFER, locations_vbo)
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, None)
        glEnableVertexAttribArray(0)
        glBindBuffer(GL_ARRAY_BUFFER, hazards_vbo)
        glVertexAttribIPointer(1, 1, GL_UNSIGNED_INT, 4, None)
        glEnableVertexAttribArray(1)

        # Set up the vao for the quad
        quad_vao = glGenVertexArrays(1)
        glBindVertexArray(quad_vao)
        glBindBuffer(GL_ARRAY_BUFFER, quad_vbo)
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, None)
        glEnableVertexAttribArray(0)

        glBindVertexArray(0)

        # Load and compile shaders
        places_program = load_shader("places")
        grid_program = load_shader("grid")

        # Enable OpenGL features
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

        # Initialise Camera position
        position = np.array([0.0, 0.0, 0.05], dtype=np.float32)

        # Imgui styling
        style = imgui.get_style()
        set_styles(style)

        # Make a guess on font size
        font_scale = 0.5
        self.update_font_scale(font_scale)

        # Initialise viewport based on framebuffer
        width, height = glfw.get_framebuffer_size(window)
        glViewport(0, 0, width, height)

        self.simulator = simulator
        self.snapshot = snapshot
        self.initial_state_snapshot = copy.deepcopy(snapshot)
        self.params = Params.fromarray(snapshot.buffers.params)
        self.nplaces = nplaces
        self.npeople = npeople
        self.nlines = nlines
        self.width = width
        self.height = height
        self.platform = platform
        self.device = device
        self.window = window
        self.first = True
        self.impl = impl
        self.font = font
        self.font_scale = font_scale

        self.simulation_active = False
        self.do_lockdown = False
        self.point_size = 2.0
        self.show_grid = True
        self.show_points = True
        self.show_lines = False
        self.show_parameters = False
        self.show_saveas = False
        self.spacing = 40.0
        self.move_sensitivity = 10.0
        self.zoom_multiplier = 1.01
        self.position = position
        self.snapshot_dir = "microsim/opencl/snapshots"
        self.snapshots = [
            f for f in os.listdir(self.snapshot_dir) if f.endswith(".npz")
        ]
        self.current_snapshot = self.snapshots.index(f"{snapshot.name}.npz")
        self.selected_snapshot = self.current_snapshot
        self.saveas_file = self.snapshots[self.current_snapshot]
        self.summary = Summary(snapshot, store_detailed_counts=False)

        self.quad_vbo = quad_vbo
        self.locations_vbo = locations_vbo
        self.hazards_vbo = hazards_vbo
        self.links_ebo = links_ebo
        self.point_vao = point_vao
        self.line_vao = line_vao
        self.quad_vao = quad_vao
        self.places_program = places_program
        self.grid_program = grid_program

        self.upload_hazards(self.snapshot.buffers.place_hazards)
        self.upload_locations(self.snapshot.buffers.place_coords)
        self.upload_links(self.snapshot.buffers.people_place_ids)

    def resize_callback(self, window, width, height):
        """Framebuffer resize callback."""
        self.width = width
        self.height = height
        glViewport(0, 0, width, height)

    def key_callback(self, window, key, scancode, action, mods):
        """Callback for keyboard controls that must fire exactly once."""
        self.impl.keyboard_callback(window, key, scancode, action, mods)
        if imgui.get_io().want_capture_keyboard or not action == glfw.PRESS:
            return

        # Show / hide lines
        elif key == glfw.KEY_L:
            self.show_lines = not self.show_lines

        # Change point size
        elif key == glfw.KEY_1:
            self.point_size = 1.0
        elif key == glfw.KEY_2:
            self.point_size = 2.0
        elif key == glfw.KEY_3:
            self.point_size = 3.0
        elif key == glfw.KEY_4:
            self.point_size = 4.0
        elif key == glfw.KEY_5:
            self.point_size = 5.0
        elif key == glfw.KEY_6:
            self.point_size = 6.0

    def is_pressed(self, key):
        return glfw.get_key(self.window, key) == glfw.PRESS

    def update_camera(self):
        if imgui.get_io().want_capture_keyboard:
            return

        # Move Camera Position
        if self.is_pressed(glfw.KEY_W):
            self.position[1] += self.move_sensitivity * self.position[2]
        if self.is_pressed(glfw.KEY_S):
            self.position[1] -= self.move_sensitivity * self.position[2]
        if self.is_pressed(glfw.KEY_A):
            self.position[0] -= self.move_sensitivity * self.position[2]
        if self.is_pressed(glfw.KEY_D):
            self.position[0] += self.move_sensitivity * self.position[2]

        # Zoom in / out
        if self.is_pressed(glfw.KEY_UP):
            self.position[2] /= self.zoom_multiplier
        if self.is_pressed(glfw.KEY_DOWN):
            self.position[2] *= self.zoom_multiplier

    def upload_locations(self, locations, lat=50.7184, lon=-3.5339):
        """Reprojects the lat lons around the provided one and uploads them to OpenGL.

        Args:
            locations: A numpy array of 2 * nplaces float32 lat/lons.
            lat: The latitude to transform the coordinates around.
            lon: The longitude to transform the coordinates around.
        """
        glBindBuffer(GL_ARRAY_BUFFER, self.locations_vbo)
        glBufferSubData(GL_ARRAY_BUFFER, 0, 2 * 4 * self.nplaces,
                        latlon_to_km(locations, lat, lon))

    def upload_hazards(self, hazards):
        """Transfers the contents of hazards to the hazards vertex buffer.

        Args:
            hazards: A numpy array of nplaces uint32 hazards.
        """
        glBindBuffer(GL_ARRAY_BUFFER, self.hazards_vbo)
        glBufferSubData(GL_ARRAY_BUFFER, 0, 4 * self.nplaces, hazards)

    def upload_links(self, place_ids):
        """Transforms the 1D place_ids buffer into a 1d element buffer and uploads it.

        Args:
            place_ids: A numpy array of npeople*nslots uint32 place IDs.
        """
        place_mat = np.reshape(
            place_ids, (self.npeople, int(place_ids.size / self.npeople)))
        place_mat = place_mat[:, 0:self.nlines]
        starts = np.repeat(place_mat[:, 0], self.nlines)
        ends = place_mat.flatten()
        links = np.empty(2 * self.npeople * self.nlines, dtype=np.uint32)
        links[0::2] = starts
        links[1::2] = ends
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.links_ebo)
        glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0,
                        2 * 4 * self.npeople * self.nlines, links)

    def update_font_scale(self, font_scale):
        """Updates the imgui font scale, starts out at 0.5"""
        imgui.get_io().font_global_scale = font_scale

    def draw_grid(self):
        """Draws a full screen quad with a grid in the pixel shader."""
        viewport = np.array([self.width, self.height],
                            dtype=np.float32)  # Viewport size
        position = self.position[0:2]  # Position in km from origin
        scale = self.position[2]  # Scale in km per pixel
        if self.width * scale / self.spacing > 60.0:
            self.spacing *= 2.0
        elif self.width * scale / self.spacing < 30.0:
            self.spacing /= 2.0
        spacing = self.spacing

        glBindVertexArray(self.quad_vao)
        glUseProgram(self.grid_program)
        glUniform2fv(glGetUniformLocation(self.grid_program, "viewport"), 1,
                     viewport)
        glUniform2fv(glGetUniformLocation(self.grid_program, "position"), 1,
                     position)
        glUniform1fv(glGetUniformLocation(self.grid_program, "spacing"), 1,
                     spacing)
        glUniform1fv(glGetUniformLocation(self.grid_program, "scale"), 1,
                     scale)
        glDrawArrays(GL_TRIANGLES, 0, 6)

    def draw_points(self):
        """Draws a point for each location colored by its hazard."""
        viewport = np.array([self.width, self.height],
                            dtype=np.float32)  # Viewport size
        position = self.position[0:2]  # Position in km from origin
        scale = self.position[2]  # Scale in km per pixel

        glBindVertexArray(self.point_vao)
        glUseProgram(self.places_program)
        glUniform2fv(glGetUniformLocation(self.places_program, "viewport"), 1,
                     viewport)
        glUniform2fv(glGetUniformLocation(self.places_program, "position"), 1,
                     position)
        glUniform1fv(glGetUniformLocation(self.places_program, "scale"), 1,
                     scale)
        glUniform1fv(glGetUniformLocation(self.places_program, "alpha"), 1,
                     1.0)
        glDrawArrays(GL_POINTS, 0, self.nplaces)

    def draw_lines(self):
        """Draws a line between each connected location colored by hazards."""
        viewport = np.array([self.width, self.height],
                            dtype=np.float32)  # Viewport size
        position = self.position[0:2]  # Position in km from origin
        scale = self.position[2]  # Scale in km per pixel

        glBindVertexArray(self.line_vao)
        glUseProgram(self.places_program)
        glUniform2fv(glGetUniformLocation(self.places_program, "viewport"), 1,
                     viewport)
        glUniform2fv(glGetUniformLocation(self.places_program, "position"), 1,
                     position)
        glUniform1fv(glGetUniformLocation(self.places_program, "scale"), 1,
                     scale)
        glUniform1fv(glGetUniformLocation(self.places_program, "alpha"), 1,
                     0.01)
        glDrawElements(GL_LINES, 2 * self.npeople * self.nlines,
                       GL_UNSIGNED_INT, None)

    def draw_platform_window(self, width, height):
        imgui.set_next_window_size(width / 6, height / 4)
        imgui.set_next_window_position(width * 5 / 6, 0)

        imgui.begin("Information", flags=default_flags)
        imgui.text(f"Platform:\n\t{self.platform}")
        imgui.text(f"Device: \n\t{self.device}")
        imgui.text(f"Snapshot:\n\t{self.snapshots[self.current_snapshot]}")
        imgui.text(f"Day:\n\t{self.simulator.time}")
        imgui.end()

    def draw_controls_window(self, width, height):
        imgui.set_next_window_size(width / 6, height / 4)
        imgui.set_next_window_position(width * 5 / 6, height * 1 / 4)

        imgui.begin("Controls", flags=default_flags)
        imgui.text("WASD to move around")
        imgui.text("Arrow Up/Down to zoom in/out")
        imgui.text("1-6 to change point size")
        if imgui.button("Stop" if self.simulation_active else "Start"):
            self.simulation_active = not self.simulation_active
        if imgui.button("Step"):
            self.update_sim()
        if imgui.button("Rollback"):
            # reset snapshot to the initial state when the inspector was created
            self.snapshot = copy.deepcopy(self.initial_state_snapshot)
            self.simulator.upload_all(self.snapshot.buffers)
            self.simulator.time = self.snapshot.time
        _, self.do_lockdown = imgui.checkbox("Lockdown", self.do_lockdown)
        if imgui.button("Hide Parameters" if self.
                        show_parameters else "Show Parameters"):
            self.show_parameters = not self.show_parameters
        imgui.end()

    def draw_layers_window(self, width, height):
        imgui.set_next_window_size(width / 6, height / 4)
        imgui.set_next_window_position(width * 5 / 6, height * 2 / 4)
        imgui.begin("Layers", flags=default_flags)
        _, self.show_grid = imgui.checkbox("Show Grid", self.show_grid)
        _, self.show_points = imgui.checkbox("Show Places", self.show_points)
        _, self.show_lines = imgui.checkbox("Show Connections",
                                            self.show_lines)
        _, self.point_size = imgui.slider_int("Point Size",
                                              self.point_size,
                                              min_value=1,
                                              max_value=6)
        clicked, self.font_scale = imgui.slider_float("Font Scale",
                                                      self.font_scale, 0.1,
                                                      0.9, "%.1f")
        if clicked:
            self.update_font_scale(self.font_scale)
        imgui.end()

    def draw_snapshots_window(self, width, height):
        imgui.set_next_window_size(width / 6, height / 4)
        imgui.set_next_window_position(width * 5 / 6, height * 3 / 4)
        imgui.begin("Snapshots", flags=default_flags)
        clicked, self.selected_snapshot = imgui.listbox(
            "", self.selected_snapshot, self.snapshots)
        if imgui.button("Load Selected"):
            self.snapshot = Snapshot.load_full_snapshot(
                f"snapshots/{self.snapshots[self.selected_snapshot]}")
            self.simulator.upload_all(self.snapshot.buffers)
            self.simulator.time = self.snapshot.time
            self.upload_hazards(self.snapshot.buffers.place_hazards)
            self.upload_locations(self.snapshot.buffers.place_coords)
            self.upload_links(self.snapshot.buffers.people_place_ids)
            self.current_snapshot = self.selected_snapshot
        if imgui.button("Save"):
            self.simulator.download_all(self.snapshot.buffers)
            self.snapshot.time = self.simulator.time
            self.snapshot.save(
                f"snapshots/{self.snapshots[self.current_snapshot]}")
        if imgui.button("Save As..."):
            self.show_saveas = True
        imgui.end()

    def draw_timeseries_window(self, width, height):
        imgui.set_next_window_size(width / 10, height)
        imgui.set_next_window_position(0, 0)
        imgui.begin("Time Series", flags=default_flags)
        graph_size = [width / 11, height / 7.5]
        self.summary.draw_plots(self.simulator.time, graph_size)
        imgui.end()

    def draw_saveas_window(self, width, height):
        imgui.set_next_window_size(width * 2 / 8, height / 8)
        imgui.begin("Save Snapshot As...", flags=imgui.WINDOW_NO_RESIZE)
        _, self.saveas_file = imgui.input_text("Filename", self.saveas_file,
                                               256)
        if imgui.button("Save"):
            self.simulator.download_all(self.snapshot.buffers)
            self.snapshot.time = self.simulator.time
            self.snapshot.save(f"snapshots/{self.saveas_file}")
            self.snapshots = [
                f for f in os.listdir("snapshots") if f.endswith(".npz")
            ]
            self.current_snapshot = self.snapshots.index(self.saveas_file)
            self.selected_snapshot = self.current_snapshot
            self.show_saveas = False
        imgui.end()

    def draw_parameters_window(self):
        """UI window with sliders for changing parameter values."""
        imgui.begin("Parameter Editor")

        imgui.text("Behaviour Change")

        _, self.params.symptomatic_multiplier = imgui.slider_float(
            "Symptomatic Multiplier", self.params.symptomatic_multiplier, 0.0,
            1.0)

        imgui.text("Duration Distributions")

        _, self.params.exposed_scale = imgui.slider_float(
            "Exposed Weibull Scale", self.params.exposed_scale, 1.0, 10.0)
        _, self.params.exposed_shape = imgui.slider_float(
            "Exposed Weibull Shape", self.params.exposed_shape, 0.0, 10.0)
        _, self.params.presymptomatic_scale = imgui.slider_float(
            "Presymptomatic Weibull Scale", self.params.presymptomatic_scale,
            0.0, 10.0)
        _, self.params.presymptomatic_shape = imgui.slider_float(
            "Presymptomatic Weibull Shape", self.params.presymptomatic_shape,
            0.0, 10.0)
        _, self.params.infection_log_scale = imgui.slider_float(
            "Infection Log-normal Scale", self.params.infection_log_scale, 0.0,
            5.0)
        _, self.params.infection_mode = imgui.slider_float(
            "Infection Log-normal Mode", self.params.infection_mode, 0.0, 20.0)

        imgui.text("Activity Hazard Multipliers")

        for i, activity in enumerate(list(Activity)):
            _, self.params.place_hazard_multipliers[i] = imgui.slider_float(
                activity.name, self.params.place_hazard_multipliers[i], 0.0,
                1.0, "%.4f")

        imgui.text("Mortality Probabilities by Age")

        _, self.params.mortality_probs[0] = imgui.slider_float(
            "0 to 4", self.params.mortality_probs[0], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[1] = imgui.slider_float(
            "5 to 9", self.params.mortality_probs[1], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[2] = imgui.slider_float(
            "10 to 14", self.params.mortality_probs[2], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[3] = imgui.slider_float(
            "15 to 19", self.params.mortality_probs[3], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[4] = imgui.slider_float(
            "20 to 24", self.params.mortality_probs[4], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[5] = imgui.slider_float(
            "25 to 29", self.params.mortality_probs[5], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[6] = imgui.slider_float(
            "30 to 34", self.params.mortality_probs[6], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[7] = imgui.slider_float(
            "35 to 39", self.params.mortality_probs[7], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[8] = imgui.slider_float(
            "40 to 44", self.params.mortality_probs[8], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[9] = imgui.slider_float(
            "45 to 49", self.params.mortality_probs[9], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[10] = imgui.slider_float(
            "50 to 54", self.params.mortality_probs[10], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[11] = imgui.slider_float(
            "55 to 59", self.params.mortality_probs[11], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[12] = imgui.slider_float(
            "60 to 64", self.params.mortality_probs[12], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[13] = imgui.slider_float(
            "65 to 69", self.params.mortality_probs[13], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[14] = imgui.slider_float(
            "70 to 74", self.params.mortality_probs[14], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[15] = imgui.slider_float(
            "75 to 79", self.params.mortality_probs[15], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[16] = imgui.slider_float(
            "80 to 84", self.params.mortality_probs[16], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[17] = imgui.slider_float(
            "85 to 89", self.params.mortality_probs[17], 0.0, 1.0, "%.7f")
        _, self.params.mortality_probs[18] = imgui.slider_float(
            "90 and above", self.params.mortality_probs[18], 0.0, 1.0, "%.7f")

        if imgui.button("Reset to Defaults"):
            self.params = Params()

        imgui.end()

    def draw_imgui(self):
        """Draws the ImGui overlay."""
        # These must be used, not self.width, self.height which are framebuffer sizes.
        width, height = glfw.get_window_size(self.window)

        self.draw_platform_window(width, height)
        self.draw_controls_window(width, height)
        self.draw_layers_window(width, height)
        self.draw_snapshots_window(width, height)
        self.draw_timeseries_window(width, height)

        if self.show_saveas:
            self.draw_saveas_window(width, height)

        if self.show_parameters:
            self.draw_parameters_window()

    def is_active(self):
        """Cycles a frame and returns whether the window remains open.

        Returns:
           True if the application is still open, otherwise False.
        """
        active = not glfw.window_should_close(self.window)

        if active and not self.first:
            imgui.pop_font()
            imgui.render()
            self.impl.render(imgui.get_draw_data())
            glfw.swap_buffers(self.window)

        if active:
            glfw.poll_events()
            self.impl.process_inputs()
            imgui.new_frame()
            imgui.push_font(self.font)
            self.first = False

        return active

    def draw(self):
        """Renders the UI."""
        self.update_camera()
        if self.show_grid:
            self.draw_grid()
        else:
            glClearColor(0.1, 0.1, 0.1, 1.0)
            glClear(GL_COLOR_BUFFER_BIT)
        if self.show_points:
            glPointSize(self.point_size)
            self.draw_points()
        if self.show_lines:
            self.draw_lines()
        self.draw_imgui()

    def update_sim(self):
        """Run a step of the simulation."""

        # Update the lockdown and upload params
        if self.do_lockdown:
            self.params.set_lockdown_multiplier(
                self.snapshot.lockdown_multipliers, self.simulator.time)
        else:
            self.params.lockdown_multiplier = 1.0  # NB: Multiplier of 1.0 has no effect
        self.simulator.upload("params", self.params.asarray())

        # Run one timestep of the model
        self.simulator.step()

        # Download hazard data from OpenCL to the host
        self.simulator.download("place_hazards",
                                self.snapshot.buffers.place_hazards)

        # Upload hazard data from the host to OpenGL
        self.upload_hazards(self.snapshot.buffers.place_hazards)

        # Download status data from OpenCL to the host
        self.simulator.download("people_statuses",
                                self.snapshot.buffers.people_statuses)

        # Compute summary statistics for hazards
        self.summary.update(self.simulator.time - 1,
                            self.snapshot.buffers.people_statuses)

    def update(self):
        """Update loop for running the simulation and updating/rendering the UI."""

        if self.simulation_active:
            self.update_sim()

        self.draw()