diff --git a/benchmarks/noop-bench-async.csv b/benchmarks/noop-bench-async.csv new file mode 100644 index 0000000..63c47cb --- /dev/null +++ b/benchmarks/noop-bench-async.csv @@ -0,0 +1,12 @@ +command,mean,stddev,median,user,system,min,max,parameter_sim +python noop/mesa_batchrunner.py,1.3258325165599998,0.05822826666377271,1.31279976286,1.2978164199999997,0.25767558,1.2780627573599999,1.46763559736,mesa_batchrunner +python noop/mesa_simulation.py,1.3915081544599999,0.07311646048704976,1.37166811936,1.35267662,0.29222067999999995,1.32746067836,1.58495303336,mesa_simulation +python noop/soil_step.py,1.9859962588599998,0.12143759641749913,1.93586195486,2.0000750199999997,0.54126188,1.9061700903599998,2.2532835533599997,soil_step +python noop/soil_step_pqueue.py,2.1347049971600005,0.01336179424666973,2.13492341986,2.1368160200000004,0.56862948,2.11810132936,2.16042739636,soil_step_pqueue +python noop/soil_gens.py,2.1284937893599998,0.03030587681163665,2.13585231586,2.14158812,0.54900038,2.0768625143599997,2.19043625236,soil_gens +python noop/soil_gens_pqueue.py,2.3469003942599995,0.019461346004472344,2.3486906343599996,2.36505852,0.54629858,2.31766326036,2.37998102136,soil_gens_pqueue +python noop/soil_async.py,2.85755484126,0.0314955571121844,2.84774029536,2.86388112,0.55261338,2.81428668936,2.90567961636,soil_async +python noop/soil_async_pqueue.py,3.1999731134600005,0.04432336803797717,3.20255954186,3.2162337199999995,0.5501872800000001,3.1406816913599997,3.26137401936,soil_async_pqueue +python noop/soilent_step.py,1.30038977816,0.017973958957989845,1.30187804986,1.3231730199999998,0.5452653799999999,1.27058263436,1.31902240836,soilent_step +python noop/soilent_step_pqueue.py,1.4708435788599998,0.027193290392962755,1.4707784423599999,1.4900387199999998,0.54749428,1.43498127536,1.53065598436,soilent_step_pqueue +python noop/soilent_gens.py,1.6338810973599998,0.05752539125688073,1.63513330036,1.65216122,0.51846678,1.54135944036,1.7038832853599999,soilent_gens diff --git a/benchmarks/noop-bench.csv b/benchmarks/noop-bench.csv new file mode 100644 index 0000000..4e2045f --- /dev/null +++ b/benchmarks/noop-bench.csv @@ -0,0 +1,11 @@ +command,mean,stddev,median,user,system,min,max,parameter_sim +python noop/mesa1_batchrunner.py,1.2559917394000002,0.012031173494887278,1.2572688413000002,1.2168630799999998,0.31825289999999995,1.2346063853,1.2735512493,mesa1_batchrunner +python noop/mesa1_simulation.py,1.3024417227,0.022498874113931668,1.2994157323,1.2595484799999999,0.3087897,1.2697029703,1.3350640403,mesa1_simulation +python noop/soil1.py,1.8789492443,0.18023367899835044,1.8186795393000001,1.86076288,0.5309521,1.7326687413000001,2.2928370642999996,soil1 +python noop/soil1_pqueue.py,1.9841675890000001,0.01735524088843906,1.9884363323,2.01830338,0.5787977999999999,1.9592171483,2.0076169282999996,soil1_pqueue +python noop/soil2.py,2.0135188921999996,0.02869307129649681,2.0184709453,2.03951308,0.5885591,1.9680417823,2.0567112592999997,soil2 +python noop/soil2_pqueue.py,2.2367320454999997,0.024339667344486046,2.2357249777999995,2.2515216799999997,0.5978869,2.1957917303,2.2688685033,soil2_pqueue +python noop/soilent1.py,1.1309301329,0.015133005948737871,1.1276461497999999,1.14056688,0.6027519,1.1135821423,1.1625753893,soilent1 +python noop/soilent1_pqueue.py,1.3097537665000003,0.018821977712258842,1.3073709358,1.3270259799999997,0.6000067999999998,1.2874580013,1.3381646823,soilent1_pqueue +python noop/soilent2.py,1.5055360476,0.05166674417574119,1.4883118568,1.5121205799999997,0.5817363999999999,1.4490918363,1.6005909333000001,soilent2 +python noop/soilent2_pqueue.py,1.6622598218,0.031130739036296016,1.6588702603,1.6862567799999997,0.5854159,1.6289724583,1.7330545383,soilent2_pqueue diff --git a/benchmarks/noop/_config.py b/benchmarks/noop/_config.py new file mode 100644 index 0000000..f6e5486 --- /dev/null +++ b/benchmarks/noop/_config.py @@ -0,0 +1,25 @@ +import os + +NUM_AGENTS = int(os.environ.get('NUM_AGENTS', 100)) +NUM_ITERS = int(os.environ.get('NUM_ITERS', 10)) +MAX_STEPS = int(os.environ.get('MAX_STEPS', 1000)) + + +def run_sim(model, **kwargs): + from soil import Simulation + opts = dict(model=model, + dump=False, + num_processes=1, + parameters={'num_agents': NUM_AGENTS}, + max_steps=MAX_STEPS, + iterations=NUM_ITERS) + opts.update(kwargs) + res = Simulation(**opts).run() + + total = sum(a.num_calls for e in res for a in e.schedule.agents) + expected = NUM_AGENTS * NUM_ITERS * MAX_STEPS + print(total) + print(expected) + + assert total == expected + return res diff --git a/benchmarks/noop/mesa_batchrunner.py b/benchmarks/noop/mesa_batchrunner.py new file mode 100644 index 0000000..326691f --- /dev/null +++ b/benchmarks/noop/mesa_batchrunner.py @@ -0,0 +1,44 @@ +from mesa import batch_run, DataCollector, Agent, Model +from mesa.time import RandomActivation + + +class NoopAgent(Agent): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.num_calls = 0 + + def step(self): + # import pdb;pdb.set_trace() + self.num_calls += 1 + + +class NoopModel(Model): + def __init__(self, N): + super().__init__() + self.schedule = RandomActivation(self) + for i in range(N): + self.schedule.add(NoopAgent(self.next_id(), self)) + self.datacollector = DataCollector(model_reporters={"num_agents": lambda m: m.schedule.get_agent_count(), + "time": lambda m: m.schedule.time}, + agent_reporters={"num_calls": "num_calls"}) + self.datacollector.collect(self) + + def step(self): + self.schedule.step() + self.datacollector.collect(self) + + +if __name__ == "__main__": + from _config import * + + res = batch_run(model_cls=NoopModel, + max_steps=MAX_STEPS, + iterations=NUM_ITERS, + number_processes=1, + parameters={'N': NUM_AGENTS}) + total = sum(s["num_calls"] for s in res) + total_agents = sum(s["num_agents"] for s in res) + assert len(res) == NUM_AGENTS * NUM_ITERS + assert total == NUM_AGENTS * NUM_ITERS * MAX_STEPS + assert total_agents == NUM_AGENTS * NUM_AGENTS * NUM_ITERS + diff --git a/benchmarks/noop/mesa_simulation.py b/benchmarks/noop/mesa_simulation.py new file mode 100644 index 0000000..7da7e85 --- /dev/null +++ b/benchmarks/noop/mesa_simulation.py @@ -0,0 +1,38 @@ +from mesa import batch_run, DataCollector, Agent, Model +from mesa.time import RandomActivation +from soil import Simulation +from _config import * + + +class NoopAgent(Agent): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.num_calls = 0 + + def step(self): + # import pdb;pdb.set_trace() + self.num_calls += 1 + + +class NoopModel(Model): + def __init__(self, num_agents, *args, **kwargs): + super().__init__() + self.schedule = RandomActivation(self) + for i in range(num_agents): + self.schedule.add(NoopAgent(self.next_id(), self)) + self.datacollector = DataCollector(model_reporters={"num_agents": lambda m: m.schedule.get_agent_count(), + "time": lambda m: m.schedule.time}, + agent_reporters={"num_calls": "num_calls"}) + self.datacollector.collect(self) + + def step(self): + self.schedule.step() + self.datacollector.collect(self) + + +def run(): + run_sim(model=NoopModel) + + +if __name__ == "__main__": + run() diff --git a/benchmarks/noop/noop-bench.csv b/benchmarks/noop/noop-bench.csv new file mode 100644 index 0000000..31c4311 --- /dev/null +++ b/benchmarks/noop/noop-bench.csv @@ -0,0 +1,3 @@ +command,mean,stddev,median,user,system,min,max,parameter_sim +python mesa1_batchrunner.py,1.2932078178200002,0.05649377020829272,1.2705532802200001,1.25902256,0.27242284,1.22210926572,1.40867459172,mesa1_batchrunner +python mesa1_simulation.py,1.81112963812,0.015491072368938567,1.81342524572,1.8594407599999996,0.8005329399999999,1.78538603972,1.84176361172,mesa1_simulation diff --git a/benchmarks/noop/soil_async.py b/benchmarks/noop/soil_async.py new file mode 100644 index 0000000..576d388 --- /dev/null +++ b/benchmarks/noop/soil_async.py @@ -0,0 +1,24 @@ +from soil import BaseAgent, Environment, Simulation + + +class NoopAgent(BaseAgent): + num_calls = 0 + + async def step(self): + while True: + self.num_calls += 1 + await self.delay() + + +class NoopEnvironment(Environment): + num_agents = 100 + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + run_sim(model=NoopEnvironment) diff --git a/benchmarks/noop/soil_async_pqueue.py b/benchmarks/noop/soil_async_pqueue.py new file mode 100644 index 0000000..ac3d2c5 --- /dev/null +++ b/benchmarks/noop/soil_async_pqueue.py @@ -0,0 +1,25 @@ +from soil import BaseAgent, Environment, Simulation, PQueueActivation + + +class NoopAgent(BaseAgent): + num_calls = 0 + + async def step(self): + while True: + self.num_calls += 1 + await self.delay() + + +class NoopEnvironment(Environment): + num_agents = 100 + schedule_class = PQueueActivation + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + run_sim(model=NoopEnvironment) diff --git a/benchmarks/noop/soil_gens.py b/benchmarks/noop/soil_gens.py new file mode 100644 index 0000000..8c128ca --- /dev/null +++ b/benchmarks/noop/soil_gens.py @@ -0,0 +1,24 @@ +from soil import BaseAgent, Environment, Simulation + + +class NoopAgent(BaseAgent): + num_calls = 0 + + def step(self): + while True: + self.num_calls += 1 + yield self.delay() + + +class NoopEnvironment(Environment): + num_agents = 100 + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + run_sim(model=NoopEnvironment) diff --git a/benchmarks/noop/soil_gens_pqueue.py b/benchmarks/noop/soil_gens_pqueue.py new file mode 100644 index 0000000..1ba3713 --- /dev/null +++ b/benchmarks/noop/soil_gens_pqueue.py @@ -0,0 +1,25 @@ +from soil import BaseAgent, Environment, Simulation, PQueueActivation + + +class NoopAgent(BaseAgent): + num_calls = 0 + + def step(self): + while True: + self.num_calls += 1 + yield self.delay() + + +class NoopEnvironment(Environment): + num_agents = 100 + schedule_class = PQueueActivation + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + run_sim(model=NoopEnvironment) diff --git a/benchmarks/noop/soil_step.py b/benchmarks/noop/soil_step.py new file mode 100644 index 0000000..2f362c7 --- /dev/null +++ b/benchmarks/noop/soil_step.py @@ -0,0 +1,21 @@ +from soil import BaseAgent, Environment, Simulation + + +class NoopAgent(BaseAgent): + num_calls = 0 + + def step(self): + self.num_calls += 1 + +class NoopEnvironment(Environment): + num_agents = 100 + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + run_sim(model=NoopEnvironment) diff --git a/benchmarks/noop/soil_step_pqueue.py b/benchmarks/noop/soil_step_pqueue.py new file mode 100644 index 0000000..6982778 --- /dev/null +++ b/benchmarks/noop/soil_step_pqueue.py @@ -0,0 +1,22 @@ +from soil import BaseAgent, Environment, Simulation, PQueueActivation + + +class NoopAgent(BaseAgent): + num_calls = 0 + + def step(self): + self.num_calls += 1 + +class NoopEnvironment(Environment): + num_agents = 100 + schedule_class = PQueueActivation + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + run_sim(model=NoopEnvironment) diff --git a/benchmarks/noop/soilent_async.py b/benchmarks/noop/soilent_async.py new file mode 100644 index 0000000..f3027c9 --- /dev/null +++ b/benchmarks/noop/soilent_async.py @@ -0,0 +1,29 @@ +from soil import Agent, Environment, Simulation +from soilent import Scheduler + + +class NoopAgent(Agent): + num_calls = 0 + + async def step(self): + while True: + self.num_calls += 1 + # yield self.delay(1) + await self.delay() + + +class NoopEnvironment(Environment): + num_agents = 100 + schedule_class = Scheduler + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + res = run_sim(model=NoopEnvironment) + for r in res: + assert isinstance(r.schedule, Scheduler) diff --git a/benchmarks/noop/soilent_async_pqueue.py b/benchmarks/noop/soilent_async_pqueue.py new file mode 100644 index 0000000..37a41d8 --- /dev/null +++ b/benchmarks/noop/soilent_async_pqueue.py @@ -0,0 +1,27 @@ +from soil import Agent, Environment +from soilent import PQueueScheduler + + +class NoopAgent(Agent): + num_calls = 0 + + async def step(self): + while True: + self.num_calls += 1 + await self.delay() + +class NoopEnvironment(Environment): + num_agents = 100 + schedule_class = PQueueScheduler + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + res = run_sim(model=NoopEnvironment) + for r in res: + assert isinstance(r.schedule, PQueueScheduler) diff --git a/benchmarks/noop/soilent_gens.py b/benchmarks/noop/soilent_gens.py new file mode 100644 index 0000000..823966f --- /dev/null +++ b/benchmarks/noop/soilent_gens.py @@ -0,0 +1,28 @@ +from soil import Agent, Environment, Simulation +from soilent import Scheduler + + +class NoopAgent(Agent): + num_calls = 0 + + def step(self): + while True: + self.num_calls += 1 + # yield self.delay(1) + yield self.delay() + +class NoopEnvironment(Environment): + num_agents = 100 + schedule_class = Scheduler + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + res = run_sim(model=NoopEnvironment) + for r in res: + assert isinstance(r.schedule, Scheduler) diff --git a/benchmarks/noop/soilent_gens_pqueue.py b/benchmarks/noop/soilent_gens_pqueue.py new file mode 100644 index 0000000..5545dc9 --- /dev/null +++ b/benchmarks/noop/soilent_gens_pqueue.py @@ -0,0 +1,28 @@ +from soil import Agent, Environment +from soilent import PQueueScheduler + + +class NoopAgent(Agent): + num_calls = 0 + + def step(self): + while True: + self.num_calls += 1 + # yield self.delay(1) + yield + +class NoopEnvironment(Environment): + num_agents = 100 + schedule_class = PQueueScheduler + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + + res = run_sim(model=NoopEnvironment) + for r in res: + assert isinstance(r.schedule, PQueueScheduler) diff --git a/benchmarks/noop/soilent_step.py b/benchmarks/noop/soilent_step.py new file mode 100644 index 0000000..9c766f2 --- /dev/null +++ b/benchmarks/noop/soilent_step.py @@ -0,0 +1,24 @@ +from soil import BaseAgent, Environment, Simulation +from soilent import Scheduler + + +class NoopAgent(BaseAgent): + num_calls = 0 + + def step(self): + self.num_calls += 1 + +class NoopEnvironment(Environment): + num_agents = 100 + schedule_class = Scheduler + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + res = run_sim(model=NoopEnvironment) + for r in res: + assert isinstance(r.schedule, Scheduler) diff --git a/benchmarks/noop/soilent_step_pqueue.py b/benchmarks/noop/soilent_step_pqueue.py new file mode 100644 index 0000000..50dca26 --- /dev/null +++ b/benchmarks/noop/soilent_step_pqueue.py @@ -0,0 +1,24 @@ +from soil import BaseAgent, Environment, Simulation +from soilent import PQueueScheduler + + +class NoopAgent(BaseAgent): + num_calls = 0 + + def step(self): + self.num_calls += 1 + +class NoopEnvironment(Environment): + num_agents = 100 + schedule_class = PQueueScheduler + + def init(self): + self.add_agents(NoopAgent, k=self.num_agents) + self.add_agent_reporter("num_calls") + + +if __name__ == "__main__": + from _config import * + res = run_sim(model=NoopEnvironment) + for r in res: + assert isinstance(r.schedule, PQueueScheduler) diff --git a/benchmarks/run.py b/benchmarks/run.py new file mode 100755 index 0000000..3e042d9 --- /dev/null +++ b/benchmarks/run.py @@ -0,0 +1,19 @@ +#!/bin/env python +import sys +import os +import subprocess +import argparse +parser = argparse.ArgumentParser( + prog='Profiler for soil') +parser.add_argument('--suffix', default=None) +parser.add_argument('files', nargs="+") + +args = parser.parse_args() + +for fname in args.files: + suffix = ("_" + args.suffix) if args.suffix else "" + simname = f"{fname.replace('/', '-')}{suffix}" + profpath = os.path.join("profs", simname + ".prof") + + print(f"Running {fname} and saving profile to {profpath}") + subprocess.call(["python", "-m", "cProfile", "-o", profpath, fname]) diff --git a/benchmarks/virusonnetwork.csv b/benchmarks/virusonnetwork.csv new file mode 100644 index 0000000..53f304d --- /dev/null +++ b/benchmarks/virusonnetwork.csv @@ -0,0 +1,4 @@ +command,mean,stddev,median,user,system,min,max,parameter_sim +python virusonnetwork/mesa_basic.py,3.8381473157,0.0518143371442526,3.8475315791,3.873109219999999,0.55102658,3.7523016936,3.9095182436,mesa_basic.py +python virusonnetwork/soil_step.py,3.2167258977000004,0.02337131987357665,3.2257620261,3.28374132,0.51343958,3.1792271306,3.2511521286000002,soil_step.py +python virusonnetwork/soil_states.py,3.4908183217,0.03726734070349347,3.4912775086,3.5684004200000006,0.50416068,3.4272087936,3.5529207346000002,soil_states.py diff --git a/benchmarks/virusonnetwork/_config.py b/benchmarks/virusonnetwork/_config.py new file mode 100644 index 0000000..79c6751 --- /dev/null +++ b/benchmarks/virusonnetwork/_config.py @@ -0,0 +1,32 @@ +import os + +NUM_AGENTS = int(os.environ.get('NUM_AGENTS', 100)) +NUM_ITERS = int(os.environ.get('NUM_ITERS', 10)) +MAX_STEPS = int(os.environ.get('MAX_STEPS', 1000)) + + +def run_sim(model, **kwargs): + from soil import Simulation + opts = dict(model=model, + dump=False, + num_processes=1, + parameters={'num_nodes': NUM_AGENTS, + "avg_node_degree": 3, + "initial_outbreak_size": 5, + "virus_spread_chance": 0.25, + "virus_check_frequency": 0.25, + "recovery_chance": 0.3, + "gain_resistance_chance": 0.1, + }, + max_steps=MAX_STEPS, + iterations=NUM_ITERS) + opts.update(kwargs) + its = Simulation(**opts).run() + + assert all(it.schedule.steps == MAX_STEPS for it in its) + ratios = list(it.resistant_susceptible_ratio() for it in its) + print("Max - Avg - Min ratio:", max(ratios), sum(ratios)/len(ratios), min(ratios)) + assert all(sum([it.number_susceptible, + it.number_infected, + it.number_resistant]) == NUM_AGENTS for it in its) + return its \ No newline at end of file diff --git a/benchmarks/virusonnetwork/mesa_basic.py b/benchmarks/virusonnetwork/mesa_basic.py new file mode 100644 index 0000000..a7d4a41 --- /dev/null +++ b/benchmarks/virusonnetwork/mesa_basic.py @@ -0,0 +1,180 @@ +# Verbatim copy from mesa +# https://github.com/projectmesa/mesa/blob/976ddfc8a1e5feaaf8007a7abaa9abc7093881a0/examples/virus_on_network/virus_on_network/model.py +import math +from enum import Enum +import networkx as nx + +import mesa + + +class State(Enum): + SUSCEPTIBLE = 0 + INFECTED = 1 + RESISTANT = 2 + + +def number_state(model, state): + return sum(1 for a in model.grid.get_all_cell_contents() if a.state is state) + + +def number_infected(model): + return number_state(model, State.INFECTED) + + +def number_susceptible(model): + return number_state(model, State.SUSCEPTIBLE) + + +def number_resistant(model): + return number_state(model, State.RESISTANT) + + +class VirusOnNetwork(mesa.Model): + """A virus model with some number of agents""" + + def __init__( + self, + *args, + num_nodes=10, + avg_node_degree=3, + initial_outbreak_size=1, + virus_spread_chance=0.4, + virus_check_frequency=0.4, + recovery_chance=0.3, + gain_resistance_chance=0.5, + **kwargs, + ): + + self.num_nodes = num_nodes + prob = avg_node_degree / self.num_nodes + self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=prob) + self.grid = mesa.space.NetworkGrid(self.G) + self.schedule = mesa.time.RandomActivation(self) + self.initial_outbreak_size = ( + initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes + ) + self.virus_spread_chance = virus_spread_chance + self.virus_check_frequency = virus_check_frequency + self.recovery_chance = recovery_chance + self.gain_resistance_chance = gain_resistance_chance + + self.datacollector = mesa.DataCollector( + { + "Ratio": "resistant_susceptible_ratio", + "Infected": number_infected, + "Susceptible": number_susceptible, + "Resistant": number_resistant, + } + ) + + # Create agents + for i, node in enumerate(self.G.nodes()): + a = VirusAgent( + i, + self, + State.SUSCEPTIBLE, + self.virus_spread_chance, + self.virus_check_frequency, + self.recovery_chance, + self.gain_resistance_chance, + ) + self.schedule.add(a) + # Add the agent to the node + self.grid.place_agent(a, node) + + # Infect some nodes + infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size) + for a in self.grid.get_cell_list_contents(infected_nodes): + a.state = State.INFECTED + + self.running = True + self.datacollector.collect(self) + + @property + def number_susceptible(self): + return number_susceptible(self) + @property + def number_resistant(self): + return number_resistant(self) + @property + def number_infected(self): + return number_infected(self) + + def resistant_susceptible_ratio(self): + try: + return number_state(self, State.RESISTANT) / number_state( + self, State.SUSCEPTIBLE + ) + except ZeroDivisionError: + return math.inf + + def step(self): + self.schedule.step() + # collect data + self.datacollector.collect(self) + + def run_model(self, n): + for i in range(n): + self.step() + + +class VirusAgent(mesa.Agent): + def __init__( + self, + unique_id, + model, + initial_state, + virus_spread_chance, + virus_check_frequency, + recovery_chance, + gain_resistance_chance, + ): + super().__init__(unique_id, model) + + self.state = initial_state + + self.virus_spread_chance = virus_spread_chance + self.virus_check_frequency = virus_check_frequency + self.recovery_chance = recovery_chance + self.gain_resistance_chance = gain_resistance_chance + + def try_to_infect_neighbors(self): + neighbors_nodes = self.model.grid.get_neighbors(self.pos, include_center=False) + susceptible_neighbors = [ + agent + for agent in self.model.grid.get_cell_list_contents(neighbors_nodes) + if agent.state is State.SUSCEPTIBLE + ] + for a in susceptible_neighbors: + if self.random.random() < self.virus_spread_chance: + a.state = State.INFECTED + + def try_gain_resistance(self): + if self.random.random() < self.gain_resistance_chance: + self.state = State.RESISTANT + + def try_remove_infection(self): + # Try to remove + if self.random.random() < self.recovery_chance: + # Success + self.state = State.SUSCEPTIBLE + self.try_gain_resistance() + else: + # Failed + self.state = State.INFECTED + + def try_check_situation(self): + if self.random.random() < self.virus_check_frequency: + # Checking... + if self.state is State.INFECTED: + self.try_remove_infection() + + def step(self): + if self.state is State.INFECTED: + self.try_to_infect_neighbors() + self.try_check_situation() + + +from _config import run_sim + +run_sim(model=VirusOnNetwork) \ No newline at end of file diff --git a/benchmarks/virusonnetwork/soil_states.py b/benchmarks/virusonnetwork/soil_states.py new file mode 100644 index 0000000..266843a --- /dev/null +++ b/benchmarks/virusonnetwork/soil_states.py @@ -0,0 +1,92 @@ +# Verbatim copy from mesa +# https://github.com/projectmesa/mesa/blob/976ddfc8a1e5feaaf8007a7abaa9abc7093881a0/examples/virus_on_network/virus_on_network/model.py +import math +from enum import Enum +import networkx as nx + +from soil import * + + +class VirusOnNetwork(Environment): + """A virus model with some number of agents""" + num_nodes = 10 + avg_node_degree = 3 + initial_outbreak_size = 1 + virus_spread_chance = 0.4 + virus_check_frequency = 0.4 + recovery_chance = 0 + gain_resistance_chance = 0 + + def init(self): + prob = self.avg_node_degree / self.num_nodes + # Use internal seed with the networkx generator + self.create_network(generator=nx.erdos_renyi_graph, n=self.num_nodes, p=prob) + + self.initial_outbreak_size = min(self.initial_outbreak_size, self.num_nodes) + self.populate_network(VirusAgent) + + # Infect some nodes + infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size) + for a in self.agents(node_id=infected_nodes): + a.set_state(VirusAgent.infected) + assert self.number_infected == self.initial_outbreak_size + + @report + def resistant_susceptible_ratio(self): + try: + return self.number_resistant / self.number_susceptible + except ZeroDivisionError: + return math.inf + + @report + @property + def number_infected(self): + return self.count_agents(state_id=VirusAgent.infected.id) + + @report + @property + def number_susceptible(self): + return self.count_agents(state_id=VirusAgent.susceptible.id) + + @report + @property + def number_resistant(self): + return self.count_agents(state_id=VirusAgent.resistant.id) + + +class VirusAgent(Agent): + virus_spread_chance = None # Inherit from model + virus_check_frequency = None # Inherit from model + recovery_chance = None # Inherit from model + gain_resistance_chance = None # Inherit from model + just_been_infected = False + + @state(default=True) + def susceptible(self): + if self.just_been_infected: + self.just_been_infected = False + return self.infected + + @state + def infected(self): + susceptible_neighbors = self.get_neighbors(state_id=self.susceptible.id) + for a in susceptible_neighbors: + if self.prob(self.virus_spread_chance): + a.just_been_infected = True + if self.prob(self.virus_check_frequency): + if self.prob(self.recovery_chance): + if self.prob(self.gain_resistance_chance): + return self.resistant + else: + return self.susceptible + else: + return self.infected + + @state + def resistant(self): + return self.at(INFINITY) + + +if __name__ == "__main__": + from _config import run_sim + run_sim(model=VirusOnNetwork) \ No newline at end of file diff --git a/benchmarks/virusonnetwork/soil_step.py b/benchmarks/virusonnetwork/soil_step.py new file mode 100644 index 0000000..1b91b2a --- /dev/null +++ b/benchmarks/virusonnetwork/soil_step.py @@ -0,0 +1,104 @@ +# Verbatim copy from mesa +# https://github.com/projectmesa/mesa/blob/976ddfc8a1e5feaaf8007a7abaa9abc7093881a0/examples/virus_on_network/virus_on_network/model.py +import math +from enum import Enum +import networkx as nx + +from soil import * + + +class State(Enum): + SUSCEPTIBLE = 0 + INFECTED = 1 + RESISTANT = 2 + + +class VirusOnNetwork(Environment): + """A virus model with some number of agents""" + num_nodes = 10 + avg_node_degree = 3 + initial_outbreak_size = 1 + virus_spread_chance = 0.4 + virus_check_frequency = 0.4 + recovery_chance = 0 + gain_resistance_chance = 0 + + def init(self): + prob = self.avg_node_degree / self.num_nodes + # Use internal seed with the networkx generator + self.create_network(generator=nx.erdos_renyi_graph, n=self.num_nodes, p=prob) + + self.initial_outbreak_size = min(self.initial_outbreak_size, self.num_nodes) + self.populate_network(VirusAgent) + + # Infect some nodes + infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size) + for a in self.agents(node_id=infected_nodes): + a.status = State.INFECTED + assert self.number_infected == self.initial_outbreak_size + + @report + def resistant_susceptible_ratio(self): + try: + return self.number_resistant / self.number_susceptible + except ZeroDivisionError: + return math.inf + + @report + @property + def number_infected(self): + return self.count_agents(status=State.INFECTED) + + @report + @property + def number_susceptible(self): + return self.count_agents(status=State.SUSCEPTIBLE) + + @report + @property + def number_resistant(self): + return self.count_agents(status=State.RESISTANT) + + + +class VirusAgent(Agent): + status = State.SUSCEPTIBLE + virus_spread_chance = None # Inherit from model + virus_check_frequency = None # Inherit from model + recovery_chance = None # Inherit from model + gain_resistance_chance = None # Inherit from model + + def try_to_infect_neighbors(self): + susceptible_neighbors = self.get_neighbors(status=State.SUSCEPTIBLE) + for a in susceptible_neighbors: + if self.prob(self.virus_spread_chance): + a.status = State.INFECTED + + def try_gain_resistance(self): + if self.prob(self.gain_resistance_chance): + self.status = State.RESISTANT + return self.at(INFINITY) + + def try_remove_infection(self): + # Try to remove + if self.prob(self.recovery_chance): + # Success + self.status = State.SUSCEPTIBLE + return self.try_gain_resistance() + + def try_check_situation(self): + if self.prob(self.virus_check_frequency): + # Checking... + if self.status is State.INFECTED: + return self.try_remove_infection() + + def step(self): + if self.status is State.INFECTED: + self.try_to_infect_neighbors() + return self.try_check_situation() + + + +if __name__ == "__main__": + from _config import run_sim + run_sim(model=VirusOnNetwork) \ No newline at end of file