1
0
mirror of https://github.com/gsi-upm/soil synced 2025-08-24 03:52:20 +00:00

Large set of changes for v0.30

The examples weren't being properly tested in the last commit. When we fixed
that a lot of bugs in the new implementation of environment and agent were
found, which accounts for most of these changes.

The main difference is the mechanism to load simulations from a configuration
file. For that to work, we had to rework our module loading code in
`serialization` and add a `source_file` attribute to configurations (and
simulations, for that matter).
This commit is contained in:
J. Fernando Sánchez
2023-04-14 19:41:24 +02:00
parent 73282530fd
commit feab0ba79e
36 changed files with 739 additions and 875 deletions

View File

@@ -1,49 +0,0 @@
---
version: '2'
name: simple
group: tests
dir_path: "/tmp/"
num_trials: 3
max_time: 100
interval: 1
seed: "CompleteSeed!"
model_class: Environment
model_params:
topology:
params:
generator: complete_graph
n: 4
agents:
agent_class: CounterModel
state:
group: network
times: 1
topology: true
distribution:
- agent_class: CounterModel
weight: 0.25
state:
state_id: 0
times: 1
- agent_class: AggregatedCounter
weight: 0.5
state:
times: 2
override:
- filter:
node_id: 1
state:
name: 'Node 1'
- filter:
node_id: 2
state:
name: 'Node 2'
fixed:
- agent_class: BaseAgent
hidden: true
topology: false
state:
name: 'Environment Agent 1'
times: 10
group: environment
am_i_complete: true

View File

@@ -1,37 +0,0 @@
---
name: simple
group: tests
dir_path: "/tmp/"
num_trials: 3
max_time: 100
interval: 1
seed: "CompleteSeed!"
network_params:
generator: complete_graph
n: 4
network_agents:
- agent_class: CounterModel
weight: 0.25
state:
state_id: 0
times: 1
- agent_class: AggregatedCounter
weight: 0.5
state:
times: 2
environment_agents:
- agent_id: 'Environment Agent 1'
agent_class: BaseAgent
state:
times: 10
environment_class: Environment
environment_params:
am_i_complete: true
agent_class: CounterModel
default_state:
times: 1
states:
1:
name: 'Node 1'
2:
name: 'Node 2'

View File

@@ -22,7 +22,9 @@ class TestAgents(TestCase):
def test_die_raises_exception(self):
"""A dead agent should raise an exception if it is stepped after death"""
d = Dead(unique_id=0, model=environment.Environment())
assert d.alive
d.step()
assert not d.alive
with pytest.raises(stime.DeadAgent):
d.step()
@@ -161,3 +163,15 @@ class TestAgents(TestCase):
assert sum(pings) == sum(range(time)) * 2
# It is the same as pings, without the leading 0
assert sum(pongs) == sum(range(time)) * 2
def test_agent_filter(self):
e = environment.Environment()
e.add_agent(agent_class=agents.BaseAgent)
e.add_agent(agent_class=agents.Evented)
base = list(e.agents(agent_class=agents.BaseAgent))
assert len(base) == 2
ev = list(e.agents(agent_class=agents.Evented))
assert len(ev) == 1
assert ev[0].unique_id == 1
null = list(e.agents(unique_ids=[0, 1], agent_class=agents.NetworkAgent))
assert not null

View File

@@ -23,86 +23,18 @@ def isequal(a, b):
assert a == b
@skip("new versions of soil do not rely on configuration files")
# @skip("new versions of soil do not rely on configuration files")
class TestConfig(TestCase):
def test_conversion(self):
expected = serialization.load_file(join(ROOT, "complete_converted.yml"))[0]
old = serialization.load_file(join(ROOT, "old_complete.yml"))[0]
converted_defaults = config.convert_old(old, strict=False)
converted = converted_defaults.dict(exclude_unset=True)
isequal(converted, expected)
def test_configuration_changes(self):
"""
The configuration should not change after running
the simulation.
"""
config = serialization.load_file(join(EXAMPLES, "complete.yml"))[0]
s = simulation.from_config(config)
init_config = copy.copy(s.to_dict())
s.run_simulation(dry_run=True)
nconfig = s.to_dict()
# del nconfig['to
isequal(init_config, nconfig)
def test_topology_config(self):
netconfig = config.NetConfig(**{"path": join(ROOT, "test.gexf")})
net = network.from_config(netconfig, dir_path=ROOT)
assert len(net.nodes) == 2
assert len(net.edges) == 1
def test_env_from_config(self):
"""
Simple configuration that tests that the graph is loaded, and that
network agents are initialized properly.
"""
cfg = {
"name": "CounterAgent",
"model_params": {
"topology": join(ROOT, "test.gexf"),
"agent_class": "CounterModel",
},
# 'states': [{'times': 10}, {'times': 20}],
"max_time": 2,
"dry_run": True,
"num_trials": 1,
}
s = simulation.from_config(cfg)
env = s.get_env()
assert len(env.G.nodes) == 2
assert len(env.G.edges) == 1
assert len(env.agents) == 2
assert env.agents[0].G == env.G
def test_agents_from_config(self):
"""We test that the known complete configuration produces
the right agents in the right groups"""
cfg = serialization.load_file(join(ROOT, "complete_converted.yml"))[0]
s = simulation.from_config(cfg)
env = s.get_env()
assert len(env.G.nodes) == 4
assert len(env.agents(group="network")) == 4
assert len(env.agents(group="environment")) == 1
def test_yaml(self):
"""
The YAML version of a newly created configuration should be equivalent
to the configuration file used.
Values not present in the original config file should have reasonable
defaults.
"""
with utils.timer("loading"):
config = serialization.load_file(join(EXAMPLES, "complete.yml"))[0]
s = simulation.from_config(config)
with utils.timer("serializing"):
serial = s.to_yaml()
with utils.timer("recovering"):
recovered = yaml.load(serial, Loader=yaml.FullLoader)
for (k, v) in config.items():
assert recovered[k] == v
def test_torvalds_config(self):
sim = simulation.from_config(os.path.join(ROOT, "test_config.yml"))
assert sim.interval == 2
envs = sim.run()
assert len(envs) == 1
env = envs[0]
assert env.interval == 2
assert env.count_agents() == 3
assert env.now == 20
def make_example_test(path, cfg):
@@ -116,7 +48,7 @@ def make_example_test(path, cfg):
s.num_trials = 1
if cfg.skip_test and not FORCE_TESTS:
self.skipTest('Example ignored.')
envs = s.run_simulation(dry_run=True)
envs = s.run_simulation(dump=False)
assert envs
for env in envs:
assert env

5
tests/test_config.yml Normal file
View File

@@ -0,0 +1,5 @@
---
source_file: "../examples/torvalds_sim.py"
model: "TorvaldsEnv"
max_steps: 10
interval: 2

View File

@@ -1,9 +1,12 @@
from unittest import TestCase
from unittest.case import SkipTest
import os
from os.path import join
from glob import glob
from soil import simulation, config, do_not_run
from soil import simulation
ROOT = os.path.abspath(os.path.dirname(__file__))
EXAMPLES = join(ROOT, "..", "examples")
@@ -16,44 +19,54 @@ class TestExamples(TestCase):
pass
def get_test_for_sim(sim, path):
def get_test_for_sims(sims, path):
root = os.getcwd()
iterations = sim.max_steps * sim.num_trials
if iterations < 0 or iterations > 1000:
sim.max_steps = 100
sim.num_trials = 1
def wrapped(self):
envs = sim.run_simulation(dry_run=True)
assert envs
for env in envs:
assert env
try:
n = sim.model_params["network_params"]["n"]
assert len(list(env.network_agents)) == n
except KeyError:
pass
assert env.schedule.steps > 0 # It has run
assert env.schedule.steps <= sim.max_steps # But not further than allowed
run = False
for sim in sims:
if sim.skip_test and not FORCE_TESTS:
continue
run = True
iterations = sim.max_steps * sim.num_trials
if iterations < 0 or iterations > 1000:
sim.max_steps = 100
sim.num_trials = 1
envs = sim.run_simulation(dump=False)
assert envs
for env in envs:
assert env
assert env.now > 0
try:
n = sim.model_params["network_params"]["n"]
assert len(list(env.network_agents)) == n
except KeyError:
pass
assert env.schedule.steps > 0 # It has run
assert env.schedule.steps <= sim.max_steps # But not further than allowed
if not run:
raise SkipTest("Example ignored because all simulations are set up to be skipped.")
return wrapped
def add_example_tests():
sim_paths = []
sim_paths = {}
for path in glob(join(EXAMPLES, '**', '*.yml')):
if "soil_output" in path:
continue
if path not in sim_paths:
sim_paths[path] = []
for sim in simulation.iter_from_config(path):
sim_paths.append((sim, path))
sim_paths[path].append(sim)
for path in glob(join(EXAMPLES, '**', '*_sim.py')):
if path not in sim_paths:
sim_paths[path] = []
for sim in simulation.iter_from_py(path):
sim_paths.append((sim, path))
sim_paths[path].append(sim)
for (sim, path) in sim_paths:
if sim.skip_test and not FORCE_TESTS:
continue
test_case = get_test_for_sim(sim, path)
for (path, sims) in sim_paths.items():
test_case = get_test_for_sims(sims, path)
fname = os.path.basename(path)
test_case.__name__ = "test_example_file_%s" % fname
test_case.__doc__ = "%s should be a valid configuration" % fname

View File

@@ -10,6 +10,8 @@ from soil import environment
from soil import simulation
from soil import agents
from mesa import Agent as MesaAgent
class Dummy(exporters.Exporter):
started = False
@@ -41,14 +43,15 @@ class Exporters(TestCase):
# ticks every step
class SimpleEnv(environment.Environment):
def init(self):
self.add_agent(agent_class=agents.BaseAgent)
self.add_agent(agent_class=MesaAgent)
num_trials = 5
max_time = 2
s = simulation.Simulation(num_trials=num_trials, max_time=max_time, name="exporter_sim", dry_run=True, model=SimpleEnv)
s = simulation.Simulation(num_trials=num_trials, max_time=max_time, name="exporter_sim",
dump=False, model=SimpleEnv)
for env in s.run_simulation(exporters=[Dummy], dry_run=True):
for env in s.run_simulation(exporters=[Dummy], dump=False):
assert len(env.agents) == 1
assert Dummy.started
@@ -60,18 +63,20 @@ class Exporters(TestCase):
assert Dummy.total_time == max_time * num_trials
def test_writing(self):
"""Try to write CSV, sqlite and YAML (without dry_run)"""
"""Try to write CSV, sqlite and YAML (without no_dump)"""
n_trials = 5
n_nodes = 4
max_time = 2
config = {
"name": "exporter_sim",
"model_params": {
"network_generator": "complete_graph",
"network_params": {"n": 4},
"network_params": {"n": n_nodes},
"agent_class": "CounterModel",
},
"max_time": 2,
"max_time": max_time,
"num_trials": n_trials,
"dry_run": False,
"dump": True,
}
output = io.StringIO()
s = simulation.from_config(config)
@@ -87,7 +92,7 @@ class Exporters(TestCase):
"constant": lambda x: 1,
},
},
dry_run=False,
dump=True,
outdir=tmpdir,
exporter_params={"copy_to": output},
)
@@ -100,12 +105,13 @@ class Exporters(TestCase):
try:
for e in envs:
db = sqlite3.connect(os.path.join(simdir, f"{s.name}.sqlite"))
dbpath = os.path.join(simdir, f"{s.name}.sqlite")
db = sqlite3.connect(dbpath)
cur = db.cursor()
agent_entries = cur.execute("SELECT * from agents").fetchall()
env_entries = cur.execute("SELECT * from env").fetchall()
assert len(agent_entries) > 0
assert len(env_entries) > 0
agent_entries = cur.execute("SELECT times FROM agents WHERE times > 0").fetchall()
env_entries = cur.execute("SELECT constant from env WHERE constant == 1").fetchall()
assert len(agent_entries) == n_nodes * n_trials * max_time
assert len(env_entries) == n_trials * max_time
with open(os.path.join(simdir, "{}.env.csv".format(e.id))) as f:
result = f.read()

View File

@@ -6,9 +6,11 @@ import networkx as nx
from functools import partial
from os.path import join
from soil import simulation, Environment, agents, network, serialization, utils, config
from soil import simulation, Environment, agents, network, serialization, utils, config, from_file
from soil.time import Delta
from mesa import Agent as MesaAgent
ROOT = os.path.abspath(os.path.dirname(__file__))
EXAMPLES = join(ROOT, "..", "examples")
@@ -30,11 +32,12 @@ class TestMain(TestCase):
config = {
"model_params": {
"topology": join(ROOT, "test.gexf"),
"agent_class": "NetworkAgent",
}
"agent_class": MesaAgent,
},
"max_time": 1
}
s = simulation.from_config(config)
s.run_simulation(dry_run=True)
s.run_simulation(dump=False)
def test_network_agent(self):
"""
@@ -75,7 +78,6 @@ class TestMain(TestCase):
def test_init_and_count_agents(self):
"""Agents should be properly initialized and counting should filter them properly"""
# TODO: separate this test into two or more test cases
env = Environment(topology=join(ROOT, "test.gexf"))
env.populate_network([CustomAgent.w(weight=1), CustomAgent.w(weight=3)])
assert env.agents[0].weight == 1
@@ -91,7 +93,7 @@ class TestMain(TestCase):
try:
os.chdir(os.path.dirname(pyfile))
s = simulation.from_py(pyfile)
env = s.run_simulation(dry_run=True)[0]
env = s.run_simulation(dump=False)[0]
for a in env.network_agents:
skill_level = a["skill_level"]
if a.node_id == "Torvalds":
@@ -151,7 +153,6 @@ class TestMain(TestCase):
def step(self):
nonlocal n_runs
n_runs += 1
return super().step()
n_trials = 50
max_time = 2
@@ -160,7 +161,7 @@ class TestMain(TestCase):
num_trials=n_trials,
max_time=max_time,
)
runs = list(s.run_simulation(dry_run=True))
runs = list(s.run_simulation(dump=False))
over = list(x.now for x in runs if x.now > 2)
assert len(runs) == n_trials
assert len(over) == 0
@@ -203,3 +204,21 @@ class TestMain(TestCase):
assert when == 2
when = a.step()
assert when == Delta(a.interval)
def test_load_sim(self):
"""Make sure at least one of the examples can be loaded"""
sims = from_file(os.path.join(EXAMPLES, "newsspread", "newsspread_sim.py"))
assert len(sims) == 3*3*2
for sim in sims:
assert sim
assert sim.name == "newspread_sim"
assert sim.num_trials == 5
assert sim.max_steps == 300
assert not sim.dump
assert sim.model_params
assert "ratio_dumb" in sim.model_params
assert "ratio_herd" in sim.model_params
assert "ratio_wise" in sim.model_params
assert "network_generator" in sim.model_params
assert "network_params" in sim.model_params
assert "prob_neighbor_spread" in sim.model_params

View File

@@ -79,8 +79,8 @@ class TestNetwork(TestCase):
env = environment.Environment(name="Test", topology=G)
env.populate_network(agents.NetworkAgent)
a2 = env.find_one(node_id=2)
a3 = env.find_one(node_id=3)
a2 = env.agent(node_id=2)
a3 = env.agent(node_id=3)
assert len(a2.subgraph(limit_neighbors=True)) == 2
assert len(a3.subgraph(limit_neighbors=True)) == 1
assert len(a3.subgraph(limit_neighbors=True, center=False)) == 0