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)
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)
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)
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)
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)
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
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)
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"], )
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
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 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()
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}
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 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)
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()
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)
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()