Skip to content

Commit 9fb4eff

Browse files
committed
test_continuous_observable: Add multi-agent test cases
1 parent f635d90 commit 9fb4eff

File tree

1 file changed

+280
-0
lines changed

1 file changed

+280
-0
lines changed

tests/test_continuous_observables.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,3 +604,283 @@ def check_death():
604604
model.simulator.schedule_event_absolute(check_continued, 100.0)
605605
model.simulator.schedule_event_absolute(check_death, 300.0)
606606
model.simulator.run_until(300.0)
607+
608+
609+
def test_continuous_observable_multiple_agents_independent_values():
610+
"""Test that multiple agents maintain independent continuous values."""
611+
612+
class MyAgent(Agent, HasObservables):
613+
energy = ContinuousObservable(
614+
initial_value=100.0,
615+
rate_func=lambda value, elapsed, agent: -agent.metabolic_rate
616+
)
617+
618+
def __init__(self, model, metabolic_rate):
619+
super().__init__(model)
620+
self.metabolic_rate = metabolic_rate
621+
self.energy = 100.0
622+
623+
model = SimpleModel()
624+
625+
# Create agents with different metabolic rates
626+
agent1 = MyAgent(model, metabolic_rate=1.0)
627+
agent2 = MyAgent(model, metabolic_rate=2.0)
628+
agent3 = MyAgent(model, metabolic_rate=0.5)
629+
630+
def check_values():
631+
# Each agent should deplete at their own rate
632+
assert agent1.energy == 90.0 # 100 - (1.0 * 10)
633+
assert agent2.energy == 80.0 # 100 - (2.0 * 10)
634+
assert agent3.energy == 95.0 # 100 - (0.5 * 10)
635+
636+
model.simulator.schedule_event_absolute(check_values, 10.0)
637+
model.simulator.run_until(10.0)
638+
639+
640+
def test_continuous_observable_multiple_agents_independent_thresholds():
641+
"""Test that different agents can have different thresholds."""
642+
643+
class MyAgent(Agent, HasObservables):
644+
energy = ContinuousObservable(
645+
initial_value=100.0,
646+
rate_func=lambda value, elapsed, agent: -1.0
647+
)
648+
649+
def __init__(self, model, name):
650+
super().__init__(model)
651+
self.name = name
652+
self.energy = 100.0
653+
self.threshold_crossed = False
654+
655+
def on_threshold(self, signal):
656+
if signal.direction == "down":
657+
self.threshold_crossed = True
658+
659+
model = SimpleModel()
660+
661+
# Create agents with different thresholds
662+
agent1 = MyAgent(model, "agent1")
663+
agent1.add_threshold("energy", 75.0, agent1.on_threshold)
664+
665+
agent2 = MyAgent(model, "agent2")
666+
agent2.add_threshold("energy", 25.0, agent2.on_threshold)
667+
668+
agent3 = MyAgent(model, "agent3")
669+
agent3.add_threshold("energy", 50.0, agent3.on_threshold)
670+
671+
def check_at_30():
672+
# At t=30, all agents at energy=70
673+
_ = agent1.energy
674+
_ = agent2.energy
675+
_ = agent3.energy
676+
677+
# Only agent1 should have crossed their threshold (75)
678+
assert agent1.threshold_crossed
679+
assert not agent2.threshold_crossed # Hasn't reached 25 yet
680+
assert not agent3.threshold_crossed # Hasn't reached 50 yet
681+
682+
def check_at_55():
683+
# At t=55, all agents at energy=45
684+
_ = agent1.energy
685+
_ = agent2.energy
686+
_ = agent3.energy
687+
688+
# agent1 and agent3 should have crossed
689+
assert agent1.threshold_crossed
690+
assert not agent2.threshold_crossed # Still hasn't reached 25
691+
assert agent3.threshold_crossed # Crossed 50
692+
693+
def check_at_80():
694+
# At t=80, all agents at energy=20
695+
_ = agent1.energy
696+
_ = agent2.energy
697+
_ = agent3.energy
698+
699+
# All should have crossed now
700+
assert agent1.threshold_crossed
701+
assert agent2.threshold_crossed # Finally crossed 25
702+
assert agent3.threshold_crossed
703+
704+
model.simulator.schedule_event_absolute(check_at_30, 30.0)
705+
model.simulator.schedule_event_absolute(check_at_55, 55.0)
706+
model.simulator.schedule_event_absolute(check_at_80, 80.0)
707+
model.simulator.run_until(80.0)
708+
709+
710+
def test_continuous_observable_multiple_agents_same_threshold_different_callbacks():
711+
"""Test that multiple agents can watch the same threshold value with different callbacks."""
712+
713+
class MyAgent(Agent, HasObservables):
714+
energy = ContinuousObservable(
715+
initial_value=100.0,
716+
rate_func=lambda value, elapsed, agent: -1.0
717+
)
718+
719+
def __init__(self, model, name):
720+
super().__init__(model)
721+
self.name = name
722+
self.energy = 100.0
723+
self.crossed_count = 0
724+
725+
def on_threshold(self, signal):
726+
if signal.direction == "down":
727+
self.crossed_count += 1
728+
729+
model = SimpleModel()
730+
731+
# Create multiple agents, all watching threshold at 50
732+
agents = [MyAgent(model, f"agent{i}") for i in range(5)]
733+
734+
for agent in agents:
735+
agent.add_threshold("energy", 50.0, agent.on_threshold)
736+
737+
def check_crossings():
738+
# Access all agents' energy
739+
for agent in agents:
740+
_ = agent.energy
741+
742+
# Each should have crossed independently
743+
for agent in agents:
744+
assert agent.crossed_count == 1
745+
746+
model.simulator.schedule_event_absolute(check_crossings, 60.0)
747+
model.simulator.run_until(60.0)
748+
749+
750+
def test_continuous_observable_agents_with_different_initial_values():
751+
"""Test agents starting with different energy values."""
752+
753+
class MyAgent(Agent, HasObservables):
754+
energy = ContinuousObservable(
755+
initial_value=100.0,
756+
rate_func=lambda value, elapsed, agent: -1.0
757+
)
758+
759+
def __init__(self, model, initial_energy):
760+
super().__init__(model)
761+
self.energy = initial_energy
762+
763+
model = SimpleModel()
764+
765+
# Create agents with different starting energies
766+
agent1 = MyAgent(model, initial_energy=100.0)
767+
agent2 = MyAgent(model, initial_energy=50.0)
768+
agent3 = MyAgent(model, initial_energy=150.0)
769+
770+
def check_values():
771+
# Each should deplete from their starting value
772+
assert agent1.energy == 90.0 # 100 - 10
773+
assert agent2.energy == 40.0 # 50 - 10
774+
assert agent3.energy == 140.0 # 150 - 10
775+
776+
model.simulator.schedule_event_absolute(check_values, 10.0)
777+
model.simulator.run_until(10.0)
778+
779+
780+
def test_continuous_observable_agent_interactions():
781+
"""Test agents affecting each other's continuous observables."""
782+
783+
class Predator(Agent, HasObservables):
784+
energy = ContinuousObservable(
785+
initial_value=50.0,
786+
rate_func=lambda value, elapsed, agent: -0.5
787+
)
788+
789+
def __init__(self, model):
790+
super().__init__(model)
791+
self.energy = 50.0
792+
self.kills = 0
793+
794+
def eat(self, prey):
795+
"""Eat prey and gain energy."""
796+
self.energy += 20
797+
self.kills += 1
798+
prey.die()
799+
800+
class Prey(Agent, HasObservables):
801+
energy = ContinuousObservable(
802+
initial_value=100.0,
803+
rate_func=lambda value, elapsed, agent: -1.0
804+
)
805+
806+
def __init__(self, model):
807+
super().__init__(model)
808+
self.energy = 100.0
809+
self.alive = True
810+
811+
def die(self):
812+
self.alive = False
813+
814+
model = SimpleModel()
815+
816+
predator = Predator(model)
817+
prey1 = Prey(model)
818+
prey2 = Prey(model)
819+
820+
def predator_hunts():
821+
# Predator energy should have depleted
822+
assert predator.energy == 45.0 # 50 - (0.5 * 10)
823+
824+
# Predator eats prey1
825+
predator.eat(prey1)
826+
827+
# Predator gains energy
828+
assert predator.energy == 65.0 # 45 + 20
829+
assert not prey1.alive
830+
assert prey2.alive
831+
832+
def check_final():
833+
# Predator continues depleting from boosted energy
834+
assert predator.energy == 60.0 # 65 - (0.5 * 10)
835+
836+
# prey2 continues depleting
837+
assert prey2.energy == 80.0 # 100 - (1.0 * 20)
838+
assert prey2.alive
839+
840+
model.simulator.schedule_event_absolute(predator_hunts, 10.0)
841+
model.simulator.schedule_event_absolute(check_final, 20.0)
842+
model.simulator.run_until(20.0)
843+
844+
845+
def test_continuous_observable_batch_creation_with_thresholds():
846+
"""Test batch agent creation where each agent has instance-specific thresholds."""
847+
848+
class MyAgent(Agent, HasObservables):
849+
energy = ContinuousObservable(
850+
initial_value=100.0,
851+
rate_func=lambda value, elapsed, agent: -1.0
852+
)
853+
854+
def __init__(self, model, critical_threshold):
855+
super().__init__(model)
856+
self.energy = 100.0
857+
self.critical_threshold = critical_threshold
858+
self.critical = False
859+
860+
# Each agent watches their own critical threshold
861+
self.add_threshold("energy", critical_threshold, self.on_critical)
862+
863+
def on_critical(self, signal):
864+
if signal.direction == "down":
865+
self.critical = True
866+
867+
model = SimpleModel()
868+
869+
# Create 10 agents with different critical thresholds
870+
thresholds = [90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 20.0, 10.0, 5.0]
871+
agents = [MyAgent(model, threshold) for threshold in thresholds]
872+
873+
def check_at_45():
874+
# At t=45, all agents at energy=55
875+
for agent in agents:
876+
_ = agent.energy # Trigger recalculation
877+
878+
# Agents with thresholds > 55 should be critical
879+
for agent, threshold in zip(agents, thresholds):
880+
if threshold > 55:
881+
assert agent.critical
882+
else:
883+
assert not agent.critical
884+
885+
model.simulator.schedule_event_absolute(check_at_45, 45.0)
886+
model.simulator.run_until(45.0)

0 commit comments

Comments
 (0)