diff --git a/README.md b/README.md index 7747c790..a2e9a127 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,14 @@ This repository hold the code base for the agent based model framework implement ### Requirements To run the simulations you will need python 3.8 or 3.9 and pip correspondingly. It is worth to set up a virtualenvironment using pipenv or venv for the project so that your global workspace is not polluted. -### Test -To test the code: +### Test Requirements +To test if all the requirements are ready to use: 1. Clone the repo 2. Activate your virtual environment (pipenv, venv) if you are using one 3. Move into the cloned repo where `setup.py` is located and run `pip install -e .` with that you installed the simulation package - 4. run the start entrypoint of the simulation package by running `abm-start` + 4. run the start entrypoint of the simulation package by running `playground-start` or `abm-start` + 5. If you also would like to save data you will need an InfluxDB instance. To setup one, please follow the instructions below. + 6. If you would like to run simulations in headless mode (without graphics) you will need to install xvfb first (only tested on Ubuntu) with `sudo apt-get install xvfb`. After this, you can start the simulation in headless mode by calling the `headless-abm-start` entrypoint instead of the normal `abm-start` entrypoint. ## Install Grafana and InfluxDB To monitor individual agents real time and save simulation data (i.e. write simulation data real time and save upon request at the end) we use InfluxDB and a grafana server for visualization. For this purpose you will need to install influx and grafana. If you don't do these steps you are still going to be able to run simulations, but you won't be able to save the resulting data or visualize the agent's parameters. This installation guide is only tested on Ubuntu. If you decide to use another op.system or you don't want to monitor and save simulation data, set `USE_IFDB_LOGGING` and `SAVE_CSV_FILES` parameters in the `.env` file to `0`. @@ -109,7 +111,8 @@ The package includes the following submodules: * `loader`: including all classes and methods to dynamically load data that was generated with the package. These methods are for example cvs and json readers and initializers that initialize input classes for Replay and DataAnalysis tools. * `metarunner`: including all classes and methods to run multiple simulations one after the other with programatically changed initialization parameters. The main classes are `Tunables` that define a criterion range with requested number of datapoints (e.g.: simulate with environment width from 300 to 600 via 4 datapoints). The `Constant` class that defines a fixed criterion throughout the simulations (e.g.: keep the simulation time `T` fixed at 1000). And `MetaProtocol` class that defines a batch of simulations with all combinations of the defined criteria according to the added `Tunable`s and `Constant`s. During running metaprotocols the corresponding initializations (as `.env` files) will be saved under the `data/metaprotocol/temp` folder. Only those `.env` files will be removed from here for which the simulations have been carried out, therefore the metaprotocol can be interrupted and finished later. * `monitoring`: including all methods to interface with InfluxDB, Grafana and to save the stored data from the database at the end of the simulation. The data will be saved into the `data/simualtion_data/` of the root abm folder. The data will consist of 2 relevant csv files (agent_data, resource_data) containing time series of the agent and resource patch status and a json file containing all parameters of the simulation for reproducibility. -* `simulation`: including the main `Simulation` class that defines how the environment is visualized, what interactions the user can have with the pygame environment (e.g.: via cursor or buttons), and how the environment enforces some restrictions on agents, and how resources are regenerated. +* `simulation`: including the main `Simulation` class that defines how the environment is visualized, what interactions the user can have with the pygame environment (e.g.: via cursor or buttons), and how the environment enforces some restrictions on agents, and how resources are regenerated. Furthermore a `PlaygroundSimulation` class is dedicated to provide an interactive playground where the user can explore different parameter combinations with the help of sliders and buttons. This class inherits all of it's simulation functionality from the main `Simulation` class but might change the visualization and adds additional interactive optionalities. When the framework is started as a playground, the parameters in the `.env` file don't matter anymore, but a `.env` file is still needed in the main ABM folder so that the supercalss can be initiated. +* `replay`: to explore large batches of simulated experimental data, a replay class has been implemented. To initialize the class one needs to pass the absolute path of an experiment folder generated by the metaruneer tool. Upon initialization, in case the experiment is not yet summarized into numpy arrays this step is carried out. The arrays are then read back to the memory at once. The different batches and parameter combinations can be explored with interactive GUI elements. In case the amount of data is too large, one can use undersampling of data to only include every n-th timestep in the summary arrays. ### Functionality and Behavior Here you can read about how the framework works in large scale behavior and what restrictions and assumptions we used throughout the simulation. @@ -230,3 +233,38 @@ EXPERIMENT_NAME=exp4 python home/ABM/abm/data/metarunner/experiments/exp4.py where we assume you have a `exp4.env` file in the root project folder (`home/ABM`) and you store an experiment file `exp4.py` under a dedicated path, in the example, this path is `home/ABM/abm/data/metarunner/experiments/`. Note that the env variable `EXPERIMENT_NAME` is used to show the given `MetaProtocol` instance which `.env` file it needs to use (and replace during runs). Therefore it must have a scope ONLY for the given command. If you set this varaible globally on your OS then all `MetaProtocol` instances will try to use and replace the same `.env` file and therefore during parallel runs unwanted behavior and corrupted data states can occur. The given commands are only to be used on Linux. + +### Interactive Exploration Tool +To allow users quick-and-dirty experimentation with the model framework, an interactive playground tool has been implemented. This can be started with `playground-start` after preparing the environment as described above. + +Once the playground tool has been started a window will pop up with a simulation arena on the upper right part with a given number of agnets and resources. Parameters are initialized according to the `contrib` package. These parameters can be tuned with interactive sliders on the right side of the window. To get some insights of these parameters see the env variable descriptions above or click and hold the `?` buttons next to the sliders. + +#### Resource number and radius +When changing the number of resource patches and their radii, the tool automatically adjusts these to each other so that the total covered area in the arena will not exceed 30% of the arena surface. This is necessary as resources are initialized in a way that no overlap is present. + +#### Fixed Overall Resource Units +When starting the tool the overall amount of resource units (summed over all patches of the arena) is fixed and can be controlled with the `SUM_R` slider. Changing this value will redistribute the amount of units between the patches in a way that the ratio of units in between tha patches will not change, and the depletion level of the patches also stays the same. In case this feature is turned off with the corresponding action button below the simulation arena, increasing the number of resource patches will increase the overall number of resources in the environment. + +#### Detailed Information +To get more detailed information about resource patches and agents, click and hold them with the left mouse button. Note that this alos moves the agents. Other interactions such as rotating agents, pausing the simulation, etc. are the same as in the original simulation class. In case you would like to get an insight about all agents and resources use the corresponding action button under the simulation area. Note that this can slow down the simulation significantly due to the amount of text to be rendered on the screen. + +#### Video Recording +To show the effect of parameter combinations and make experiments reproducable, you can also record a short video of particularly interesting phenomena. To do so, use the `Record Video` action button under the simulation arena. When the recording is started, the button turns red as well as a red "Rec" dot will pop up in the upper left corner. When you stop the recording with the same action button, the tool will save and compress the resulting video and save in the data folder of the package. Please note that this might take a few minutes for longer videos. + +#### Other Function Buttons +Some boolean parameters can be turned on and off with the help of additional function buttons (below the visualization area). These are + * Turn on Ghost Mode: overalpping on the patches are allowed + * Turn on IFDB logging: in case a visualization through the grafana interface is required one can start IFDB logging with this button. By default it is turned off so that we can avoid a database writing overhead and the tool can be aslo started without IFDB installed on the system. + * Turn on Visual Occlusion: in case it is turned on, agents can occlude visual cues from farther away agants. + +### Replay Tool +To visualize large batches of data generated as experiment folders with the metarunner tool, one can use the replay tool. A demonstrative script has been provided in the repo to show how one can start such a replay of experiment. + +#### Behavior +Upon start if the experiment was not summarized before into numpy arrays this will be done. Then these arrays are read back to the memory to initialize the GUI of the tool. On the left side an arena shows the agnets and resources. Below, global statistics are shown in case it is requested with the `Show Stats` action button. The path of the agents as well as their visual field can be visualized with the corresponding buttons. Note that interactions from the simulation or the playground tool won't work here as this visualization will be a pure replay (as in a replayed video) of the recorded simulation itself. One can replay the recorded data in time with the `Start/Stop` button or by moving the time slider. + +#### Parameter Combinations +Possible parameter combinations are read automatically from the data and the corresponding sliders will be initialized in the action area accordingly. By that, one can go through the simulated parameter combinations and different batches by moving the sliders on the right. + +#### Plotting +To plot some global statistics of the data corresponding action buttons have been implemented on the right. Note that it only works with 1, 2 or 3 changed parameters. In case 3 parameters were tuned throughout the experiment one can either plot multiple 2 dimensional figures or "collapse" the plot along an axis using some method, such as minimum or maximum collision. This means that along that axis instead of taking all values into consideration one will onmly take the max or min of the values. This is especially useful when 2 parameters were tuned together in a way that their product should remain the same (That can be done adding so called Tuned Pairs to the criterion of the metarunner tool). In these cases only specified parameter combinations have informative values and not the whole parameter space provided with the parameter ranges. diff --git a/abm/agent/agent.py b/abm/agent/agent.py index 448b8577..29549553 100644 --- a/abm/agent/agent.py +++ b/abm/agent/agent.py @@ -52,6 +52,7 @@ def __init__(self, id, radius, position, orientation, env_size, color, v_field_r self.position = np.array(position, dtype=np.float64) # saved self.orientation = orientation # saved self.color = color + self.selected_color = colors.LIGHT_BLUE self.v_field_res = v_field_res self.pooling_time = pooling_time self.pooling_prob = pooling_prob @@ -59,6 +60,7 @@ def __init__(self, id, radius, position, orientation, env_size, color, v_field_r self.vision_range = vision_range self.visual_exclusion = visual_exclusion self.FOV = FOV + self.show_stats = False # Non-initialisable private attributes self.velocity = 0 # agent absolute velocity # saved @@ -95,6 +97,8 @@ def __init__(self, id, radius, position, orientation, env_size, color, v_field_r self.g_u = decision_params.g_u self.B_u = decision_params.B_u self.u_max = decision_params.u_max + self.F_N = decision_params.F_N + self.F_R = decision_params.F_R # Pooling attributes self.time_spent_pooling = 0 # time units currently spent with pooling the status of given position (changes @@ -133,7 +137,7 @@ def calc_I_priv(self): collected_unit = self.collected_r - self.collected_r_before # calculating private info by weighting these - self.I_priv = decision_params.F_N * np.max(self.novelty) + decision_params.F_R * collected_unit + self.I_priv = self.F_N * np.max(self.novelty) + self.F_R * collected_unit def move_with_mouse(self, mouse, left_state, right_state): """Moving the agent with the mouse cursor, and rotating""" @@ -264,9 +268,15 @@ def draw_update(self): self.image = pygame.Surface([self.radius * 2, self.radius * 2]) self.image.fill(colors.BACKGROUND) self.image.set_colorkey(colors.BACKGROUND) - pygame.draw.circle( - self.image, self.color, (self.radius, self.radius), self.radius - ) + if self.is_moved_with_cursor: + pygame.draw.circle( + self.image, self.selected_color, (self.radius, self.radius), self.radius + ) + else: + pygame.draw.circle( + self.image, self.color, (self.radius, self.radius), self.radius + ) + # showing agent orientation with a line towards agent orientation pygame.draw.line(self.image, colors.BACKGROUND, (self.radius, self.radius), ((1 + np.cos(self.orientation)) * self.radius, (1 - np.sin(self.orientation)) * self.radius), diff --git a/abm/app.py b/abm/app.py index 2ced0ab4..1d03d619 100644 --- a/abm/app.py +++ b/abm/app.py @@ -1,43 +1,62 @@ +from contextlib import ExitStack + from abm.simulation.sims import Simulation +from abm.simulation.isims import PlaygroundSimulation + +from xvfbwrapper import Xvfb import os # loading env variables from dotenv file from dotenv import dotenv_values + EXP_NAME = os.getenv("EXPERIMENT_NAME", "") -def start(parallel=False): +def start(parallel=False, headless=False): root_abm_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) envconf = dotenv_values(os.path.join(root_abm_dir, f"{EXP_NAME}.env")) - sim = Simulation(N=int(float(envconf["N"])), - T=int(float(envconf["T"])), - v_field_res=int(envconf["VISUAL_FIELD_RESOLUTION"]), - agent_fov=float(envconf['AGENT_FOV']), - framerate=int(float(envconf["INIT_FRAMERATE"])), - with_visualization=bool(int(float(envconf["WITH_VISUALIZATION"]))), - width=int(float(envconf["ENV_WIDTH"])), - height=int(float(envconf["ENV_HEIGHT"])), - show_vis_field=bool(int(float(envconf["SHOW_VISUAL_FIELDS"]))), - show_vis_field_return=bool(int(envconf['SHOW_VISUAL_FIELDS_RETURN'])), - pooling_time=int(float(envconf["POOLING_TIME"])), - pooling_prob=float(envconf["POOLING_PROBABILITY"]), - agent_radius=int(float(envconf["RADIUS_AGENT"])), - N_resc=int(float(envconf["N_RESOURCES"])), - min_resc_perpatch=int(float(envconf["MIN_RESOURCE_PER_PATCH"])), - max_resc_perpatch=int(float(envconf["MAX_RESOURCE_PER_PATCH"])), - min_resc_quality=float(envconf["MIN_RESOURCE_QUALITY"]), - max_resc_quality=float(envconf["MAX_RESOURCE_QUALITY"]), - patch_radius=int(float(envconf["RADIUS_RESOURCE"])), - regenerate_patches=bool(int(float(envconf["REGENERATE_PATCHES"]))), - agent_consumption=int(float(envconf["AGENT_CONSUMPTION"])), - ghost_mode=bool(int(float(envconf["GHOST_WHILE_EXPLOIT"]))), - patchwise_exclusion=bool(int(float(envconf["PATCHWISE_SOCIAL_EXCLUSION"]))), - teleport_exploit=bool(int(float(envconf["TELEPORT_TO_MIDDLE"]))), - vision_range=int(float(envconf["VISION_RANGE"])), - visual_exclusion=bool(int(float(envconf["VISUAL_EXCLUSION"]))), - show_vision_range=bool(int(float(envconf["SHOW_VISION_RANGE"]))), - use_ifdb_logging=bool(int(float(envconf["USE_IFDB_LOGGING"]))), - save_csv_files=bool(int(float(envconf["SAVE_CSV_FILES"]))), - parallel=parallel - ) + vscreen_width = int(float(envconf["ENV_WIDTH"])) + 100 + vscreen_height = int(float(envconf["ENV_HEIGHT"])) + 100 + with ExitStack() if not headless else Xvfb(width=vscreen_width, height=vscreen_height) as xvfb: + sim = Simulation(N=int(float(envconf["N"])), + T=int(float(envconf["T"])), + v_field_res=int(envconf["VISUAL_FIELD_RESOLUTION"]), + agent_fov=float(envconf['AGENT_FOV']), + framerate=int(float(envconf["INIT_FRAMERATE"])), + with_visualization=bool(int(float(envconf["WITH_VISUALIZATION"]))), + width=int(float(envconf["ENV_WIDTH"])), + height=int(float(envconf["ENV_HEIGHT"])), + show_vis_field=bool(int(float(envconf["SHOW_VISUAL_FIELDS"]))), + show_vis_field_return=bool(int(envconf['SHOW_VISUAL_FIELDS_RETURN'])), + pooling_time=int(float(envconf["POOLING_TIME"])), + pooling_prob=float(envconf["POOLING_PROBABILITY"]), + agent_radius=int(float(envconf["RADIUS_AGENT"])), + N_resc=int(float(envconf["N_RESOURCES"])), + min_resc_perpatch=int(float(envconf["MIN_RESOURCE_PER_PATCH"])), + max_resc_perpatch=int(float(envconf["MAX_RESOURCE_PER_PATCH"])), + min_resc_quality=float(envconf["MIN_RESOURCE_QUALITY"]), + max_resc_quality=float(envconf["MAX_RESOURCE_QUALITY"]), + patch_radius=int(float(envconf["RADIUS_RESOURCE"])), + regenerate_patches=bool(int(float(envconf["REGENERATE_PATCHES"]))), + agent_consumption=int(float(envconf["AGENT_CONSUMPTION"])), + ghost_mode=bool(int(float(envconf["GHOST_WHILE_EXPLOIT"]))), + patchwise_exclusion=bool(int(float(envconf["PATCHWISE_SOCIAL_EXCLUSION"]))), + teleport_exploit=bool(int(float(envconf["TELEPORT_TO_MIDDLE"]))), + vision_range=int(float(envconf["VISION_RANGE"])), + visual_exclusion=bool(int(float(envconf["VISUAL_EXCLUSION"]))), + show_vision_range=bool(int(float(envconf["SHOW_VISION_RANGE"]))), + use_ifdb_logging=bool(int(float(envconf["USE_IFDB_LOGGING"]))), + save_csv_files=bool(int(float(envconf["SAVE_CSV_FILES"]))), + parallel=parallel + ) + sim.write_batch_size = 100 + sim.start() + + +def start_headless(): + start(headless=True) + + +def start_playground(): + sim = PlaygroundSimulation() sim.start() diff --git a/abm/contrib/ifdb_params.py b/abm/contrib/ifdb_params.py index 9758e2c5..3add918e 100644 --- a/abm/contrib/ifdb_params.py +++ b/abm/contrib/ifdb_params.py @@ -3,6 +3,7 @@ from dotenv import dotenv_values EXP_NAME = os.getenv("EXPERIMENT_NAME", "") +WRITE_EACH_POINT = os.getenv("WRITE_EACH_POINT") root_abm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) env_path = os.path.join(root_abm_dir, f"{EXP_NAME}.env") @@ -14,13 +15,16 @@ INFLUX_PSWD = "password" INFLUX_DB_NAME = "home" -T = float(int(envconf["T"])) -if T <= 1000: - write_batch_size = T +if WRITE_EACH_POINT is not None: + write_batch_size = 1 else: - if T % 1000 != 0: - raise Exception("Simulation time (T) must be dividable by 1000 or smaller than 1000!") - write_batch_size = 1000 + T = float(int(envconf["T"])) + if T <= 1000: + write_batch_size = T + else: + if T % 1000 != 0: + raise Exception("Simulation time (T) must be dividable by 1000 or smaller than 1000!") + write_batch_size = 1000 # SAVE_DIR is counted from the ABM parent directory. SAVE_DIR = envconf.get("SAVE_ROOT_DIR", "abm/data/simulation_data") diff --git a/abm/contrib/playgroundtool.py b/abm/contrib/playgroundtool.py new file mode 100644 index 00000000..742d5293 --- /dev/null +++ b/abm/contrib/playgroundtool.py @@ -0,0 +1,150 @@ +"""parameters for interactive playground simulations""" + +VIDEO_SAVE_DIR = "abm/data/videos" + +default_params = { + "N": 5, # interactive + "T": 100000, # interactive + "v_field_res": 1200, + "width": 500, + "height": 500, + "framerate": 30, # interactive + "window_pad": 30, + "with_visualization": True, + "show_vis_field": False, + "show_vis_field_return": True, + "pooling_time": 0, + "pooling_prob":0, + "agent_radius": 10, + "N_resc": 3, # interactive + "min_resc_perpatch": 200, + "max_resc_perpatch": 201, + "min_resc_quality": 0.25, + "max_resc_quality": 0.25, + "patch_radius": 30, # interactive + "regenerate_patches": True, + "agent_consumption": 1, + "teleport_exploit": False, + "vision_range": 5000, + "agent_fov": 0.5, # interactive + "visual_exclusion": True, # interactive + "show_vision_range": True, + "use_ifdb_logging": False, # interactive + "save_csv_files": False, + "ghost_mode": True, # interactive + "patchwise_exclusion": True, + "parallel": False +} + +help_messages = { + 'framerate': ''' + + Framerate [fps]: + + The framerate is a parameter that defines how often + (per second) is the state of the simulation is + updated. In case the simulation has many agents/resources + it might happen that the requested + framerate is impossible to keep with the given hardware. + In that case the maximal possible framerate + will be kept. + + ''', + 'N':''' + + Number of agents, N [pcs]: + + The number of agent controls how many individuals the + group consists of in terms of foraging agents visualized + as colorful circles with a white line showing their ori- + entations. The field of view of the agents are shown + with a green circle slice. + + ''', + 'N_res':''' + + Number of resource patches, N_res [pcs]: + + The number of resource patches in the environment. + Each resource patch can be exploited by agents and they + contain a given amount of resource units. The quality + of the patch controls how fast (unit/time) can be + the resource patch exploited by a single agent. Resource + patches are shown as gray circles on the screen. + + ''', + 'FOV': ''' + + Field of View, FOV [%]: + + The amount (in percent) of visible area around individual + agents. In case this is 0, the agents are blind. In case + it is 100, the agents have a full 360° FOV. + + ''', + 'RES': ''' + + Resource Radius, R [px]: + + The radius of resource patches in pixels. In case the overall + covered resource area on the arena exceeds 30% of the total space + the number of resource patches will be automatically decreased. + + ''', + 'Epsw': ''' + + Social Excitability, E_w [a.U.]: + + The parameter controls how socially excitable agents are, i.e. how + much a unit of social information can increase/bias the decision + process of the agent towards socially guided behavior (Relocation). + In case this parameter is 0, agents do not integrate social cues + at all. The larger this parameter is, the faster individuals respond + to visible exploiting agents and the farther social cues are triggering + relocation movement. + + ''', + 'Epsu': ''' + + Individual Preference Factor, E_u [a.U.]: + + The parameter controls how much a unit of exploited resource biases + the agent towards further exploitation of resources. + In case this parameter is low, agents will get picky + with resource patches, i.e. they stop exploiting low-quality + patches after an initial sampling period. + + ''', + 'SWU': ''' + + W to U Cross-inhibition strength, S_wu [a.U.]: + + The parameter controls how much socially guided behavior and + integration of social cues inhibit individual exploitation + behavior. Note, that this inhibition is only present if the + social integrator w is above it's decision threshold. + + ''', + 'SUW': ''' + + U to W Cross-inhibition strength, S_uw [a.U.]: + + The parameter controls how much individual exploitation behavior and + integration of individual information inhibits social + behavior. Note, that this inhibition is only present if the + individual integrator u is above it's decision threshold. + + ''', + 'SUMR': ''' + + Total number of resource units, SUM_r [a.U.]: + + The parameter controls how many resource units should be overall + distributed in the environment. This number can be fixed with the + "Fix Total Units" action button. In this case changing the number + of patches will redistribute the units between the patches so + that although the ratio between patches stays the same, the + number of units per patch will change. + + ''' +} \ No newline at end of file diff --git a/abm/environment/rescource.py b/abm/environment/rescource.py index 715c95e5..8328a414 100644 --- a/abm/environment/rescource.py +++ b/abm/environment/rescource.py @@ -5,6 +5,7 @@ import numpy as np from abm.contrib import colors + class Rescource(pygame.sprite.Sprite): """ Rescource class that includes all private parameters of the rescource patch and all methods necessary to exploit @@ -43,6 +44,8 @@ def __init__(self, id, radius, position, env_size, color, window_pad, resc_units self.color = color self.resc_left_color = colors.DARK_GREY self.unit_per_timestep = quality # saved + self.is_clicked = False + self.show_stats = False # Environment related parameters self.WIDTH = env_size[0] # env width @@ -65,11 +68,24 @@ def __init__(self, id, radius, position, env_size, color, window_pad, resc_units ) self.mask = pygame.mask.from_surface(self.image) self.rect = self.image.get_rect() - self.rect.x = self.position[0] - self.rect.y = self.position[1] - font = pygame.font.Font(None, 25) - text = font.render(f"{self.radius}", True, colors.BLACK) - self.image.blit(text, (0, 0)) + self.rect.centerx = self.center[0] + self.rect.centery = self.center[1] + if self.is_clicked: + font = pygame.font.Font(None, 25) + text = font.render(f"{self.radius}", True, colors.BLACK) + self.image.blit(text, (0, 0)) + + def update_clicked_status(self, mouse): + """Checking if the resource patch was clicked on a mouse event""" + if self.rect.collidepoint(mouse): + self.is_clicked = True + self.position[0] = mouse[0] - self.radius + self.position[1] = mouse[1] - self.radius + self.center = (self.position[0] + self.radius, self.position[1] + self.radius) + self.update() + else: + self.is_clicked = False + self.update() def update(self): # Initial Visualization of rescource @@ -87,10 +103,11 @@ def update(self): self.rect.centerx = self.center[0] self.rect.centery = self.center[1] self.mask = pygame.mask.from_surface(self.image) - font = pygame.font.Font(None, 18) - text = font.render(f"{self.resc_left:.2f}, Q{self.unit_per_timestep:.2f}", True, colors.BLACK) - self.image.blit(text, (0, 0)) - text_rect = text.get_rect(center=self.rect.center) + if self.is_clicked or self.show_stats: + font = pygame.font.Font(None, 18) + text = font.render(f"{self.resc_left:.2f}, Q{self.unit_per_timestep:.2f}", True, colors.BLACK) + self.image.blit(text, (0, 0)) + text_rect = text.get_rect(center=self.rect.center) def deplete(self, rescource_units): """depeting the given patch with given rescource units""" @@ -101,11 +118,10 @@ def deplete(self, rescource_units): if self.resc_left >= rescource_units: self.resc_left -= rescource_units depleted_units = rescource_units - else: # can not deplete more than what is left + else: # can not deplete more than what is left depleted_units = self.resc_left self.resc_left = 0 if self.resc_left > 0: return depleted_units, False else: return depleted_units, True - diff --git a/abm/monitoring/ifdb.py b/abm/monitoring/ifdb.py index 9a20ce86..4cc4a575 100644 --- a/abm/monitoring/ifdb.py +++ b/abm/monitoring/ifdb.py @@ -46,7 +46,7 @@ def pad_to_n_digits(number, n=3): return str(number) -def save_agent_data(ifclient, agents, exp_hash=""): +def save_agent_data(ifclient, agents, exp_hash="", batch_size=None): """Saving relevant agent data into InfluxDB intance if multiple simulations are running in parallel a uuid hash must be passed as experiment hash to find the unique measurement in the database @@ -82,7 +82,9 @@ def save_agent_data(ifclient, agents, exp_hash=""): # write the measurement in batches batch_bodies_agents.append(body) - if len(batch_bodies_agents) == ifdbp.write_batch_size: + if batch_size is None: + batch_size = ifdbp.write_batch_size + if len(batch_bodies_agents) == batch_size: ifclient.write_points(batch_bodies_agents) batch_bodies_agents = [] @@ -99,7 +101,7 @@ def mode_to_int(mode): return int(3) -def save_resource_data(ifclient, resources, exp_hash=""): +def save_resource_data(ifclient, resources, exp_hash="", batch_size=None): """Saving relevant resource patch data into InfluxDB instance if multiple simulations are running in parallel a uuid hash must be passed as experiment hash to find the unique measurement in the database""" @@ -129,7 +131,9 @@ def save_resource_data(ifclient, resources, exp_hash=""): batch_bodies_resources.append(body) # write the measurement in batches - if len(batch_bodies_resources) == ifdbp.write_batch_size: + if batch_size is None: + batch_size = ifdbp.write_batch_size + if len(batch_bodies_resources) == batch_size: ifclient.write_points(batch_bodies_resources) batch_bodies_resources = [] diff --git a/abm/replay/replay.py b/abm/replay/replay.py index 44e2ba2b..0e42688a 100644 --- a/abm/replay/replay.py +++ b/abm/replay/replay.py @@ -86,9 +86,12 @@ def __init__(self, data_folder_path, undersample=1, collapse=None): self.time_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, self.slider_height, fontSize=self.slider_height - 2, borderThickness=1) slider_i = 3 + slider_max_val = self.num_batches - 1 + if slider_max_val <= 0: + slider_max_val = 1 slider_start_y = slider_i * (self.slider_height + self.action_area_pad) self.batch_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, - self.slider_height, min=0, max=self.num_batches - 1, step=1, initial=0) + self.slider_height, min=0, max=slider_max_val, step=1, initial=0) self.batch_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, self.slider_height, fontSize=self.slider_height - 2, borderThickness=1) diff --git a/abm/simulation/isims.py b/abm/simulation/isims.py new file mode 100644 index 00000000..6852ebdf --- /dev/null +++ b/abm/simulation/isims.py @@ -0,0 +1,707 @@ +"""Implementing an interactive Playground Simulation class where the model parameters can be tuned real time""" +import shutil + +import pygame +import numpy as np +from math import floor, ceil + +from abm.contrib import colors, ifdb_params +from abm.contrib import playgroundtool as pgt +from abm.monitoring import ifdb +from abm.simulation.sims import Simulation +from pygame_widgets.slider import Slider +from pygame_widgets.button import Button +from pygame_widgets.textbox import TextBox +from abm.monitoring.ifdb import pad_to_n_digits + +import pygame_widgets +import os +import cv2 + +from datetime import datetime + +# loading env variables from dotenv file +from dotenv import dotenv_values + +EXP_NAME = os.getenv("EXPERIMENT_NAME", "") +root_abm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +env_path = os.path.join(root_abm_dir, f"{EXP_NAME}.env") + +envconf = dotenv_values(env_path) + + +class PlaygroundSimulation(Simulation): + def __init__(self): + super().__init__(**pgt.default_params) + # fixing the total number of possible resources, so it is redistributed with changing NR + self.SUM_res_fixed = True + self.prev_overall_coll_r = 0 # total collected resources in previous step + self.overall_col_r = 0 # total collected resources by all agents + self.SUM_res = self.get_total_resource() # total possible amount of resources + self.help_message = "" + self.is_help_shown = False + self.is_recording = False + self.save_video = False # trigger to save video from screenshots + self.video_save_path = os.path.join(root_abm_dir, pgt.VIDEO_SAVE_DIR) # path to save video + self.image_save_path = os.path.join(self.video_save_path, "tmp") # path from collect screenshots + self.show_all_stats = False + # enabling paths + if os.path.isdir(self.image_save_path): + shutil.rmtree(self.image_save_path) + os.makedirs(self.image_save_path, exist_ok=True) + # GUI parameters + # visualization area of the simulation + self.vis_area_end_width = 2 * self.window_pad + self.WIDTH + self.vis_area_end_height = 2 * self.window_pad + self.HEIGHT + # starting global statistics text + self.global_stats_start = self.vis_area_end_height + # start of sliders + self.action_area_width = 400 + self.action_area_height = 800 + # full window parameters + self.full_width = self.WIDTH + self.action_area_width + 2 * self.window_pad + self.full_height = self.action_area_height + + self.quit_term = False + self.screen = pygame.display.set_mode([self.full_width, self.full_height], pygame.RESIZABLE) + + # button groups + self.help_buttons = [] + self.function_buttons = [] + # sliders and other gui elements + self.sliders = [] + self.slider_texts = [] + + # pygame widgets + self.slider_height = 10 + self.textbox_height = 20 + self.help_height = self.textbox_height + self.help_width = self.help_height + self.function_button_width = 100 + self.function_button_height = 20 + self.function_button_pad = 20 + self.action_area_pad = 40 + self.textbox_width = 100 + self.slider_width = self.action_area_width - 2 * self.action_area_pad - self.textbox_width - 15 + self.slider_start_x = self.vis_area_end_width + self.action_area_pad + self.textbox_start_x = self.slider_start_x + self.slider_width + 15 + self.help_start_x = self.textbox_start_x + self.textbox_width + 15 + + ## Function Button 1st Row + function_button_start_x = self.window_pad + function_button_start_y = self.vis_area_end_height + self.start_button = Button(self.screen, function_button_start_x, function_button_start_y, + self.function_button_width, + self.function_button_height, text='Start/Stop', + fontSize=self.function_button_height - 2, + inactiveColour=colors.GREEN, borderThickness=1, onClick=lambda: self.start_stop()) + self.function_buttons.append(self.start_button) + function_button_start_x += self.function_button_width + self.function_button_pad + self.record_button = Button(self.screen, function_button_start_x, function_button_start_y, + self.function_button_width, + self.function_button_height, text='Record Video', + fontSize=self.function_button_height - 2, + inactiveColour=colors.GREY, borderThickness=1, + onClick=lambda: self.start_stop_record()) + self.function_buttons.append(self.record_button) + function_button_start_x += self.function_button_width + self.function_button_pad + self.fix_SUM_res_button = Button(self.screen, function_button_start_x, function_button_start_y, + self.function_button_width, + self.function_button_height, text='Fix Total Units', + fontSize=self.function_button_height - 2, + inactiveColour=colors.GREEN, borderThickness=1, + onClick=lambda: self.fix_SUM_res()) + self.function_buttons.append(self.fix_SUM_res_button) + function_button_start_x += self.function_button_width + self.function_button_pad + self.show_all_stats_button = Button(self.screen, function_button_start_x, function_button_start_y, + self.function_button_width, + self.function_button_height, text='Show All', + fontSize=self.function_button_height - 2, + inactiveColour=colors.GREY, borderThickness=1, + onClick=lambda: self.show_hide_all_stats()) + self.function_buttons.append(self.show_all_stats_button) + + ## Function Button Second Row + function_button_start_x = self.window_pad + function_button_start_y = self.vis_area_end_height + self.function_button_height + self.function_button_pad + self.visual_exclusion_button = Button(self.screen, function_button_start_x, function_button_start_y, + self.function_button_width, + self.function_button_height, text='Visual Occl.', + fontSize=self.function_button_height - 2, + inactiveColour=colors.GREEN, borderThickness=1, + onClick=lambda: self.change_visual_occlusion()) + self.function_buttons.append(self.visual_exclusion_button) + function_button_start_x += self.function_button_width + self.function_button_pad + self.ghost_mode_button = Button(self.screen, function_button_start_x, function_button_start_y, + self.function_button_width, + self.function_button_height, text='Ghost Mode', + fontSize=self.function_button_height - 2, + inactiveColour=colors.GREEN, borderThickness=1, + onClick=lambda: self.change_ghost_mode()) + self.function_buttons.append(self.ghost_mode_button) + function_button_start_x += self.function_button_width + self.function_button_pad + self.IFDB_button = Button(self.screen, function_button_start_x, function_button_start_y, + self.function_button_width, + self.function_button_height, text='IFDB Log', + fontSize=self.function_button_height - 2, + inactiveColour=colors.GREY, borderThickness=1, + onClick=lambda: self.start_stop_IFDB_logging()) + self.function_buttons.append(self.IFDB_button) + + self.global_stats_start += 2 * self.function_button_height + self.function_button_pad + self.window_pad + + ## First Slider column + slider_i = 1 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.framerate_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=5, max=60, step=1, initial=self.framerate) + self.framerate_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.framerate_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.framerate_help.onClick = lambda: self.show_help('framerate', self.framerate_help) + self.framerate_help.onRelease = lambda: self.unshow_help(self.framerate_help) + self.help_buttons.append(self.framerate_help) + self.sliders.append(self.framerate_slider) + self.slider_texts.append(self.framerate_textbox) + + slider_i = 2 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.N_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=1, max=35, step=1, initial=self.N) + self.N_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.N_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.N_help.onClick = lambda: self.show_help('N', self.N_help) + self.N_help.onRelease = lambda: self.unshow_help(self.N_help) + self.help_buttons.append(self.N_help) + self.sliders.append(self.N_slider) + self.slider_texts.append(self.N_textbox) + + slider_i = 3 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.NRES_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=1, max=100, step=1, initial=self.N_resc) + self.NRES_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.NRES_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.NRES_help.onClick = lambda: self.show_help('N_res', self.NRES_help) + self.NRES_help.onRelease = lambda: self.unshow_help(self.NRES_help) + self.help_buttons.append(self.NRES_help) + self.sliders.append(self.NRES_slider) + self.slider_texts.append(self.NRES_textbox) + + slider_i = 4 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.FOV_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=0, max=1, step=0.05, initial=self.agent_fov[1] / np.pi) + self.FOV_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.FOV_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.FOV_help.onClick = lambda: self.show_help('FOV', self.FOV_help) + self.FOV_help.onRelease = lambda: self.unshow_help(self.FOV_help) + self.help_buttons.append(self.FOV_help) + self.sliders.append(self.FOV_slider) + self.slider_texts.append(self.FOV_textbox) + + slider_i = 5 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.RESradius_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=10, max=100, step=5, initial=self.resc_radius) + self.RESradius_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.RES_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.RES_help.onClick = lambda: self.show_help('RES', self.RES_help) + self.RES_help.onRelease = lambda: self.unshow_help(self.RES_help) + self.help_buttons.append(self.RES_help) + self.sliders.append(self.RESradius_slider) + self.slider_texts.append(self.RESradius_textbox) + + slider_i = 6 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.Eps_w = 2 + self.Epsw_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=0, max=5, step=0.1, initial=self.Eps_w) + self.Epsw_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.Epsw_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.Epsw_help.onClick = lambda: self.show_help('Epsw', self.Epsw_help) + self.Epsw_help.onRelease = lambda: self.unshow_help(self.Epsw_help) + self.help_buttons.append(self.Epsw_help) + self.sliders.append(self.Epsw_slider) + self.slider_texts.append(self.Epsw_textbox) + + slider_i = 7 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.Eps_u = 1 + self.Epsu_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=0, max=5, step=0.1, initial=self.Eps_u) + self.Epsu_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.Epsu_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.Epsu_help.onClick = lambda: self.show_help('Epsu', self.Epsu_help) + self.Epsu_help.onRelease = lambda: self.unshow_help(self.Epsu_help) + self.help_buttons.append(self.Epsu_help) + self.sliders.append(self.Epsu_slider) + self.slider_texts.append(self.Epsu_textbox) + + slider_i = 8 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.S_wu = 0 + self.SWU_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=0, max=2, step=0.1, initial=self.S_wu) + self.SWU_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.SWU_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.SWU_help.onClick = lambda: self.show_help('SWU', self.SWU_help) + self.SWU_help.onRelease = lambda: self.unshow_help(self.SWU_help) + self.help_buttons.append(self.SWU_help) + self.sliders.append(self.SWU_slider) + self.slider_texts.append(self.SWU_textbox) + + slider_i = 9 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.S_uw = 0 + self.SUW_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=0, max=2, step=0.1, initial=self.S_uw) + self.SUW_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.SUW_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.SUW_help.onClick = lambda: self.show_help('SUW', self.SUW_help) + self.SUW_help.onRelease = lambda: self.unshow_help(self.SUW_help) + self.help_buttons.append(self.SUW_help) + self.sliders.append(self.SUW_slider) + self.slider_texts.append(self.SUW_textbox) + + slider_i = 10 + slider_start_y = slider_i * (self.slider_height + self.action_area_pad) + self.SUMR_slider = Slider(self.screen, self.slider_start_x, slider_start_y, self.slider_width, + self.slider_height, min=0, max=self.SUM_res + 200, step=100, initial=self.SUM_res) + self.SUMR_textbox = TextBox(self.screen, self.textbox_start_x, slider_start_y, self.textbox_width, + self.textbox_height, fontSize=self.textbox_height - 2, borderThickness=1) + self.SUMR_help = Button(self.screen, self.help_start_x, slider_start_y, self.help_width, self.help_height, + text='?', fontSize=self.help_height - 2, inactiveColour=colors.GREY, + borderThickness=1, ) + self.SUMR_help.onClick = lambda: self.show_help('SUMR', self.SUMR_help) + self.SUMR_help.onRelease = lambda: self.unshow_help(self.SUMR_help) + self.help_buttons.append(self.SUMR_help) + self.sliders.append(self.SUMR_slider) + self.slider_texts.append(self.SUMR_textbox) + + def start_stop_IFDB_logging(self): + """Start or stop IFDB logging in case of grafana interface is used""" + self.save_in_ifd = not self.save_in_ifd + if self.save_in_ifd: + if self.ifdb_client is None: + self.ifdb_client = ifdb.create_ifclient() + self.ifdb_client.create_database(ifdb_params.INFLUX_DB_NAME) + self.write_batch_size = 2 + self.IFDB_button.inactiveColour = colors.GREEN + else: + self.write_batch_size = None + self.IFDB_button.inactiveColour = colors.GREY + + def change_ghost_mode(self): + """Changing ghost mdoe during exploutation""" + self.ghost_mode = not self.ghost_mode + if self.ghost_mode: + self.ghost_mode_button.inactiveColour = colors.GREEN + else: + self.ghost_mode_button.inactiveColour = colors.GREY + + def change_visual_occlusion(self): + """Changing visual occlusion parameter""" + self.visual_exclusion = not self.visual_exclusion + for ag in self.agents: + ag.visual_exclusion = self.visual_exclusion + if self.visual_exclusion: + self.visual_exclusion_button.inactiveColour = colors.GREEN + else: + self.visual_exclusion_button.inactiveColour = colors.GREY + + def show_hide_all_stats(self): + """Show or hide all information""" + self.show_all_stats = not self.show_all_stats + if self.show_all_stats: + self.show_all_stats_button.inactiveColour = colors.GREEN + for ag in self.agents: + ag.show_stats = True + for res in self.rescources: + res.show_stats = True + else: + self.show_all_stats_button.inactiveColour = colors.GREY + for ag in self.agents: + ag.show_stats = False + for res in self.rescources: + res.show_stats = False + + def fix_SUM_res(self): + """Fixing total amount of possible resources so it will not change with changing the number of patches""" + self.SUM_res_fixed = not self.SUM_res_fixed + if self.SUM_res_fixed: + self.fix_SUM_res_button.inactiveColour = colors.GREEN + else: + self.fix_SUM_res_button.inactiveColour = colors.GREY + + def start_stop_record(self): + """Start or stop the recording of the simulation into a vdieo""" + if not self.is_recording: + self.is_recording = True + self.record_button.inactiveColour = colors.RED + self.record_button.string = "Stop Recording" + self.record_button.text = self.record_button.font.render(self.record_button.string, True, + self.record_button.textColour) + else: + self.is_recording = False + self.save_video = True + self.record_button.inactiveColour = colors.GREY + self.record_button.string = "Record Video" + self.record_button.text = self.record_button.font.render(self.record_button.string, True, + self.record_button.textColour) + self.help_message = "SAVING VIDEO..." + self.draw_help_message() + + def start_stop(self): + """Switch to start or stop the simulation""" + self.is_paused = not self.is_paused + if self.start_button.inactiveColour != colors.GREY: + self.start_button.inactiveColour = colors.GREY + else: + self.start_button.inactiveColour = colors.GREEN + + def show_help(self, help_decide_str, pressed_button): + """Switch to show help message""" + for hb in self.help_buttons: + hb.inactiveColour = colors.GREY + if not self.is_paused: + self.is_paused = True + self.is_help_shown = True + self.help_message = pgt.help_messages[help_decide_str] + pressed_button.inactiveColour = colors.GREEN + + def unshow_help(self, pressed_button): + """Switch to erease help message from screen""" + for hb in self.help_buttons: + hb.inactiveColour = colors.GREY + self.is_help_shown = False + if self.is_paused: + self.is_paused = False + pressed_button.inactiveColour = colors.GREY + + def update_SUMR(self): + """Updating the total possible resource units if necessary""" + self.SUM_res = self.get_total_resource() + self.SUMR_slider.min = self.N_resc + self.SUMR_slider.max = 2 * self.SUM_res + self.SUMR_slider.setValue(self.SUM_res) + + def get_total_resource(self): + """Calculating the total number of resource units in the arena""" + SUMR = 0 + for res in self.rescources: + SUMR += res.resc_units + return SUMR + + def draw_frame(self, stats, stats_pos): + """Overwritten method of sims drawframe adding possibility to update pygame widgets""" + super().draw_frame(stats, stats_pos) + self.framerate_textbox.setText(f"Framerate: {self.framerate}") + self.N_textbox.setText(f"N: {self.N}") + self.NRES_textbox.setText(f"N_R: {self.N_resc}") + self.FOV_textbox.setText(f"FOV: {int(self.fov_ratio * 100)}%") + self.RESradius_textbox.setText(f"R_R: {int(self.resc_radius)}") + self.Epsw_textbox.setText(f"E_w: {self.Eps_w:.2f}") + self.Epsu_textbox.setText(f"E_u: {self.Eps_u:.2f}") + self.SUW_textbox.setText(f"S_uw: {self.S_uw:.2f}") + self.SWU_textbox.setText(f"S_wu: {self.S_wu:.2f}") + if self.SUM_res == 0: + self.update_SUMR() + self.SUMR_textbox.setText(f"SUM R: {self.SUM_res:.2f}") + for sl in self.sliders: + sl.draw() + for slt in self.slider_texts: + slt.draw() + for hb in self.help_buttons: + hb.draw() + for fb in self.function_buttons: + fb.draw() + if self.is_help_shown: + self.draw_help_message() + self.draw_global_stats() + if self.is_recording: + self.draw_record_circle() + if self.save_video: + # Showing the help message before the screen freezes + self.help_message = "\n\n\n Saving video, please wait..." + self.draw_help_message() + pygame.display.flip() + # Save video (freezes update process for a while) + self.saved_images_to_video() + self.save_video = False + + def draw_record_circle(self): + """Drawing a red circle to show that the frame is recording""" + if self.t % 60 < 30: + circle_rad = int(self.window_pad / 4) + image = pygame.Surface([2 * circle_rad, 2 * circle_rad]) + image.fill(colors.BACKGROUND) + image.set_colorkey(colors.BACKGROUND) + pygame.draw.circle( + image, colors.RED, (circle_rad, circle_rad), circle_rad + ) + self.screen.blit(image, (circle_rad, circle_rad)) + + def draw_framerate(self): + pass + + def draw_global_stats(self): + image = pygame.Surface([self.vis_area_end_width, self.full_height]) + image.fill(colors.BACKGROUND) + image.set_colorkey(colors.BACKGROUND) + image.set_alpha(200) + line_height = 20 + font = pygame.font.Font(None, line_height) + status = [] + self.overall_col_r = np.sum([ag.collected_r for ag in self.agents]) + status.append(f"Total collected units: {self.overall_col_r:.2f} U") + status.append(f"Exploitation Rate: {self.overall_col_r - self.prev_overall_coll_r:.2f} U/timestep") + for i, stat_i in enumerate(status): + text_color = colors.BLACK + text = font.render(stat_i, True, text_color) + image.blit(text, (self.window_pad, self.global_stats_start + i * line_height)) + self.screen.blit(image, (0, 0)) + self.prev_overall_coll_r = self.overall_col_r + + def draw_help_message(self): + image = pygame.Surface([self.vis_area_end_width, self.vis_area_end_height]) + image.fill(colors.BACKGROUND) + image.set_alpha(200) + line_height = 20 + font = pygame.font.Font(None, line_height) + status = self.help_message.split("\n") + for i, stat_i in enumerate(status): + text_color = colors.BLACK + text = font.render(stat_i, True, text_color) + image.blit(text, (self.window_pad, i * line_height)) + self.screen.blit(image, (0, 0)) + + def interact_with_event(self, events): + """Carry out functionality according to user's interaction""" + super().interact_with_event(events) + pygame_widgets.update(events) + self.framerate = self.framerate_slider.getValue() + self.N = self.N_slider.getValue() + self.N_resc = self.NRES_slider.getValue() + self.fov_ratio = self.FOV_slider.getValue() + if self.N != len(self.agents): + self.act_on_N_mismatch() + if self.N_resc != len(self.rescources): + self.act_on_NRES_mismatch() + if self.fov_ratio != self.agent_fov[1] / np.pi: + self.update_agent_fovs() + if self.resc_radius != self.RESradius_slider.getValue(): + self.resc_radius = self.RESradius_slider.getValue() + self.update_res_radius() + if self.Eps_w != self.Epsw_slider.getValue(): + self.Eps_w = self.Epsw_slider.getValue() + self.update_agent_decision_params() + if self.Eps_u != self.Epsu_slider.getValue(): + self.Eps_u = self.Epsu_slider.getValue() + self.update_agent_decision_params() + if self.SUM_res != self.SUMR_slider.getValue(): + self.SUM_res = self.SUMR_slider.getValue() + self.distribute_sumR() + if self.S_uw != self.SUW_slider.getValue(): + self.S_uw = self.SUW_slider.getValue() + self.update_agent_decision_params() + if self.S_wu != self.SWU_slider.getValue(): + self.S_wu = self.SWU_slider.getValue() + self.update_agent_decision_params() + if self.is_recording: + filename = f"{pad_to_n_digits(self.t, n=6)}.jpeg" + path = os.path.join(self.image_save_path, filename) + pygame.image.save(self.screen, path) + + def distribute_sumR(self): + """If the amount of requestedtotal amount changes we decrease the amount of resource of all resources in a way that + the original resource ratios remain the same""" + resource_ratios = [] + remaining_pecents = [] + current_sum_res = self.get_total_resource() + for ri, res in enumerate(self.rescources): + resource_ratios.append(res.resc_units / current_sum_res) + remaining_pecents.append(res.resc_left / res.resc_units) + + # now changing the amount of all and remaining resources according to new sumres + for ri, res in enumerate(self.rescources): + res.resc_units = resource_ratios[ri] * self.SUM_res + res.resc_left = remaining_pecents[ri] * res.resc_units + res.update() + + self.min_resc_units = floor(self.SUM_res / self.N_resc) + self.max_resc_units = max(ceil(self.SUM_res / self.N_resc), floor(self.SUM_res / self.N_resc) + 1) + + def update_agent_decision_params(self): + """Updateing agent decision parameters according to changed slider values""" + for ag in self.agents: + ag.Eps_w = self.Eps_w + ag.Eps_u = self.Eps_u + ag.S_uw = self.S_uw + ag.S_wu = self.S_wu + + def pop_resource(self): + for res in self.rescources: + res.kill() + break + self.N_resc = len(self.rescources) + self.NRES_slider.setValue(self.N_resc) + + def update_res_radius(self): + """Changing the resource patch radius according to slider value""" + # adjusting number of patches + sum_area = len(self.rescources) * self.resc_radius * self.resc_radius * np.pi + if sum_area > 0.3 * self.WIDTH * self.HEIGHT: + while sum_area > 0.3 * self.WIDTH * self.HEIGHT: + self.pop_resource() + sum_area = len(self.rescources) * self.resc_radius * self.resc_radius * np.pi + + for res in self.rescources: + # # update position + res.position[0] = res.center[0] - self.resc_radius + res.position[1] = res.center[1] - self.resc_radius + # self.center = (self.position[0] + self.radius, self.position[1] + self.radius) + res.radius = self.resc_radius + res.rect.x = res.position[0] + res.rect.y = res.position[1] + res.update() + + def update_agent_fovs(self): + """Updateing the FOV of agents according to acquired value from slider""" + self.agent_fov = (-self.fov_ratio * np.pi, self.fov_ratio * np.pi) + for agent in self.agents: + agent.FOV = self.agent_fov + + def act_on_N_mismatch(self): + """method is called if the requested amount of agents is not the same as what the playground already has""" + if self.N > len(self.agents): + diff = self.N - len(self.agents) + for i in range(diff): + ag_id = len(self.agents) + x = np.random.randint(self.agent_radii, self.WIDTH - self.agent_radii) + y = np.random.randint(self.agent_radii, self.HEIGHT - self.agent_radii) + orient = np.random.uniform(0, 2 * np.pi) + self.add_new_agent(ag_id, x, y, orient, with_proove=False) + self.update_agent_decision_params() + self.update_agent_fovs() + else: + while self.N < len(self.agents): + for i, ag in enumerate(self.agents): + if i == len(self.agents) - 1: + ag.kill() + if self.show_all_stats: + for ag in self.agents: + ag.show_stats = True + self.stats, self.stats_pos = self.create_vis_field_graph() + + def act_on_NRES_mismatch(self): + """method is called if the requested amount of patches is not the same as what the playground already has""" + if self.N_resc > len(self.rescources): + diff = self.N_resc - len(self.rescources) + for i in range(diff): + sum_area = (len(self.rescources) + 1) * self.resc_radius * self.resc_radius * np.pi + if sum_area > 0.3 * self.WIDTH * self.HEIGHT: + while sum_area > 0.3 * self.WIDTH * self.HEIGHT: + self.resc_radius -= 5 + self.RESradius_slider.setValue(self.resc_radius) + sum_area = (len(self.rescources) + 1) * self.resc_radius * self.resc_radius * np.pi + self.update_res_radius() + else: + self.add_new_resource_patch() + else: + while self.N_resc < len(self.rescources): + for i, res in enumerate(self.rescources): + if i == len(self.rescources) - 1: + res.kill() + if not self.SUM_res_fixed: + self.update_SUMR() + else: + self.distribute_sumR() + if self.show_all_stats: + for res in self.rescources: + res.show_stats = True + + def draw_visual_fields(self): + """Visualizing the range of vision for agents as opaque circles around the agents""" + for agent in self.agents: + FOV = agent.FOV + + # Show limits of FOV + if 0 < FOV[1] < np.pi: + + # Center and radius of pie chart + cx, cy, r = agent.position[0] + agent.radius, agent.position[1] + agent.radius, 100 + + angle = (2 * FOV[1]) / np.pi * 360 + p = [(cx, cy)] + # Get points on arc + angles = [agent.orientation + FOV[0], agent.orientation + FOV[1]] + step_size = (angles[1] - angles[0]) / 50 + angles_array = np.arange(angles[0], angles[1] + step_size, step_size) + for n in angles_array: + x = cx + int(r * np.cos(n)) + y = cy + int(r * - np.sin(n)) + p.append((x, y)) + p.append((cx, cy)) + + image = pygame.Surface([self.vis_area_end_width, self.vis_area_end_height]) + image.fill(colors.BACKGROUND) + image.set_colorkey(colors.BACKGROUND) + image.set_alpha(10) + pygame.draw.polygon(image, colors.GREEN, p) + self.screen.blit(image, (0, 0)) + + elif FOV[1] == np.pi: + image = pygame.Surface([self.vis_area_end_width, self.vis_area_end_height]) + image.fill(colors.BACKGROUND) + image.set_colorkey(colors.BACKGROUND) + image.set_alpha(10) + cx, cy, r = agent.position[0] + agent.radius, agent.position[1] + agent.radius, 100 + pygame.draw.circle(image, colors.GREEN, (cx, cy), r) + self.screen.blit(image, (0, 0)) + + def saved_images_to_video(self): + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + image_folder = self.image_save_path + video_name = os.path.join(self.video_save_path, f'recording_{timestamp}.mp4') + + images = sorted([img for img in os.listdir(image_folder) if img.endswith(".jpeg")]) + frame = cv2.imread(os.path.join(image_folder, images[0])) + height, width, layers = frame.shape + + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + video = cv2.VideoWriter(video_name, fourcc, self.framerate, (width, height)) + + for image in images: + video.write(cv2.imread(os.path.join(image_folder, image))) + + cv2.destroyAllWindows() + video.release() + shutil.rmtree(self.image_save_path) + os.makedirs(self.image_save_path, exist_ok=True) diff --git a/abm/simulation/sims.py b/abm/simulation/sims.py index c3fecf9c..6186cacb 100644 --- a/abm/simulation/sims.py +++ b/abm/simulation/sims.py @@ -138,7 +138,8 @@ def __init__(self, N, T, v_field_res=800, width=600, height=480, self.agent_consumption = agent_consumption self.teleport_exploit = teleport_exploit self.vision_range = vision_range - self.agent_fov = (-agent_fov * np.pi, agent_fov * np.pi) + self.fov_ratio = agent_fov + self.agent_fov = (-self.fov_ratio * np.pi, self.fov_ratio * np.pi) self.visual_exclusion = visual_exclusion self.ghost_mode = ghost_mode self.patchwise_exclusion = patchwise_exclusion @@ -170,6 +171,7 @@ def __init__(self, N, T, v_field_res=800, width=600, height=480, self.clock = pygame.time.Clock() # Monitoring + self.write_batch_size = None self.parallel = parallel if self.parallel: self.ifdb_hash = uuid.uuid4().hex @@ -183,6 +185,8 @@ def __init__(self, N, T, v_field_res=800, width=600, height=480, self.ifdb_client.drop_database(ifdb_params.INFLUX_DB_NAME) self.ifdb_client.create_database(ifdb_params.INFLUX_DB_NAME) ifdb.save_simulation_params(self.ifdb_client, self, exp_hash=self.ifdb_hash) + else: + self.ifdb_client = None # by default we parametrize with the .env file in root folder root_abm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) @@ -262,12 +266,13 @@ def draw_framerate(self): def draw_agent_stats(self, font_size=15, spacing=0): """Showing agent information when paused""" - if self.is_paused: - font = pygame.font.Font(None, font_size) - for agent in self.agents: + # if self.is_paused: + font = pygame.font.Font(None, font_size) + for agent in self.agents: + if agent.is_moved_with_cursor or agent.show_stats: status = [ f"ID: {agent.id}", - f"res.: {agent.collected_r}", + f"res.: {agent.collected_r:.2f}", f"ori.: {agent.orientation:.2f}", f"w: {agent.w:.2f}" ] @@ -279,7 +284,11 @@ def draw_agent_stats(self, font_size=15, spacing=0): def kill_resource(self, resource): """Killing (and regenerating) a given resource patch""" if self.regenerate_resources: - self.add_new_resource_patch() + rid = self.add_new_resource_patch() + if resource.show_stats: + for res in self.rescources: + if res.id == rid: + res.show_stats = True resource.kill() def add_new_resource_patch(self): @@ -300,6 +309,7 @@ def add_new_resource_patch(self): quality) resource_proven = self.proove_sprite(resource) self.rescources.add(resource) + return resource.id def agent_agent_collision(self, agent1, agent2): """collision protocol called on any agent that has been collided with another one @@ -346,15 +356,12 @@ def agent_agent_collision(self, agent1, agent2): else: # ghost mode is on, we do nothing on collision pass - def create_agents(self): - """Creating agents according to how the simulation class was initialized""" - i = 0 - while i < self.N: - x = np.random.randint(self.agent_radii, self.WIDTH - self.agent_radii) - y = np.random.randint(self.agent_radii, self.HEIGHT - self.agent_radii) - orient = np.random.uniform(0, 2*np.pi) + def add_new_agent(self, id, x, y, orient, with_proove=True): + """Adding a single new agent into agent sprites""" + agent_proven = False + while not agent_proven: agent = Agent( - id=i, + id=id, radius=self.agent_radii, position=(x, y), orientation=orient, @@ -370,9 +377,21 @@ def create_agents(self): visual_exclusion=self.visual_exclusion, patchwise_exclusion=self.patchwise_exclusion ) - if self.proove_sprite(agent): + if with_proove: + if self.proove_sprite(agent): + self.agents.add(agent) + agent_proven = True + else: self.agents.add(agent) - i += 1 + agent_proven = True + + def create_agents(self): + """Creating agents according to how the simulation class was initialized""" + for i in range(self.N): + x = np.random.randint(self.agent_radii, self.WIDTH - self.agent_radii) + y = np.random.randint(self.agent_radii, self.HEIGHT - self.agent_radii) + orient = np.random.uniform(0, 2*np.pi) + self.add_new_agent(i, x, y, orient) def create_resources(self): """Creating resource patches according to how the simulation class was initialized""" @@ -397,46 +416,53 @@ def create_vis_field_graph(self): stats_pos = (int(self.window_pad), int(self.window_pad)) return stats, stats_pos - def interact_with_event(self, event): + def interact_with_event(self, events): """Carry out functionality according to user's interaction""" - # Exit if requested - if event.type == pygame.QUIT: - sys.exit() - - # Change orientation with mouse wheel - if event.type == pygame.MOUSEWHEEL: - if event.y == -1: - event.y = 0 - for ag in self.agents: - ag.move_with_mouse(pygame.mouse.get_pos(), event.y, 1 - event.y) - - # Pause on Space - if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: - self.is_paused = not self.is_paused - - # Speed up on s and down on f. reset default framerate with d - if event.type == pygame.KEYDOWN and event.key == pygame.K_s: - self.framerate -= 1 - if self.framerate < 1: - self.framerate = 1 - if event.type == pygame.KEYDOWN and event.key == pygame.K_f: - self.framerate += 1 - if self.framerate > 35: - self.framerate = 35 - if event.type == pygame.KEYDOWN and event.key == pygame.K_d: - self.framerate = self.framerate_orig - - # Continuous mouse events (move with cursor) - if pygame.mouse.get_pressed()[0]: - try: + for event in events: + # Exit if requested + if event.type == pygame.QUIT: + sys.exit() + + # Change orientation with mouse wheel + if event.type == pygame.MOUSEWHEEL: + if event.y == -1: + event.y = 0 for ag in self.agents: - ag.move_with_mouse(event.pos, 0, 0) - except AttributeError: + ag.move_with_mouse(pygame.mouse.get_pos(), event.y, 1 - event.y) + + # Pause on Space + if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: + self.is_paused = not self.is_paused + + # Speed up on s and down on f. reset default framerate with d + if event.type == pygame.KEYDOWN and event.key == pygame.K_s: + self.framerate -= 1 + if self.framerate < 1: + self.framerate = 1 + if event.type == pygame.KEYDOWN and event.key == pygame.K_f: + self.framerate += 1 + if self.framerate > 35: + self.framerate = 35 + if event.type == pygame.KEYDOWN and event.key == pygame.K_d: + self.framerate = self.framerate_orig + + # Continuous mouse events (move with cursor) + if pygame.mouse.get_pressed()[0]: + try: + for ag in self.agents: + ag.move_with_mouse(event.pos, 0, 0) + for res in self.rescources: + res.update_clicked_status(event.pos) + except AttributeError: + for ag in self.agents: + ag.move_with_mouse(pygame.mouse.get_pos(), 0, 0) + else: for ag in self.agents: - ag.move_with_mouse(pygame.mouse.get_pos(), 0, 0) - else: - for ag in self.agents: - ag.is_moved_with_cursor = False + ag.is_moved_with_cursor = False + ag.draw_update() + for res in self.rescources: + res.is_clicked = False + res.update() def decide_on_vis_field_visibility(self, turned_on_vfield): """Deciding f the visual field needs to be shown or not""" @@ -499,8 +525,6 @@ def draw_frame(self, stats, stats_pos): # showing visual fields of the agents self.show_visual_fields(stats, stats_pos) - pygame.display.flip() - def start(self): start_time = datetime.now() @@ -512,7 +536,7 @@ def start(self): self.create_resources() # Creating surface to show visual fields - stats, stats_pos = self.create_vis_field_graph() + self.stats, self.stats_pos = self.create_vis_field_graph() # local var to decide when to show visual fields turned_on_vfield = 0 @@ -520,9 +544,9 @@ def start(self): # Main Simulation loop until dedicated simulation time while self.t < self.T: - for event in pygame.event.get(): - # Carry out interaction according to user activity - self.interact_with_event(event) + events = pygame.event.get() + # Carry out interaction according to user activity + self.interact_with_event(events) # deciding if vis field needs to be shown in this timestep turned_on_vfield = self.decide_on_vis_field_visibility(turned_on_vfield) @@ -648,12 +672,15 @@ def start(self): # Draw environment and agents if self.with_visualization: - self.draw_frame(stats, stats_pos) + self.draw_frame(self.stats, self.stats_pos) + pygame.display.flip() # Monitoring with IFDB if self.save_in_ifd: - ifdb.save_agent_data(self.ifdb_client, self.agents, exp_hash=self.ifdb_hash) - ifdb.save_resource_data(self.ifdb_client, self.rescources, exp_hash=self.ifdb_hash) + ifdb.save_agent_data(self.ifdb_client, self.agents, exp_hash=self.ifdb_hash, + batch_size=self.write_batch_size) + ifdb.save_resource_data(self.ifdb_client, self.rescources, exp_hash=self.ifdb_hash, + batch_size=self.write_batch_size) # Moving time forward self.clock.tick(self.framerate) diff --git a/setup.py b/setup.py index 966965b1..a067a44b 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ name='P34 ABM', description='Agent based model framework to simulate collectively foraging agents relying on their private and social' 'visual cues. Written in pygame and python 3.7+', - version='0.0.1', + version='1.2.0', url='https://github.com/scioip34/ABM', maintainer='David Mezey and Dominik Deffner @ SCIoI', packages=find_packages(exclude=['tests']), @@ -20,7 +20,9 @@ 'matplotlib', 'python-dotenv', 'pandas', - 'influxdb' + 'influxdb', + 'opencv-python', + 'xvfbwrapper' ], extras_require={ 'test': [ @@ -33,6 +35,8 @@ entry_points={ 'console_scripts': [ 'abm-start=abm.app:start', + 'headless-abm-start=abm.app:start_headless', + 'playground-start=abm.app:start_playground' ] }, classifiers=[