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