@@ -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