@@ -100,24 +100,91 @@ def test_reset_generation_preserves_global_counter(self):
100100 inn = tracker .get_innovation_number (2 , 3 , 'add_connection' )
101101 self .assertEqual (inn , 4 )
102102
103- def test_add_node_mutations_tracked_separately (self ):
104- """Test that add_node mutations track both connections separately."""
103+ def test_node_split_deduplication (self ):
104+ """Test that splitting the same connection in one generation produces
105+ the same node ID and innovation numbers, regardless of which genome
106+ performs the split."""
105107 tracker = neat .InnovationTracker ()
106-
107- # Split connection 0->1 by adding node 2
108- inn_in = tracker .get_innovation_number (0 , 2 , 'add_node_in' )
109- inn_out = tracker .get_innovation_number (2 , 1 , 'add_node_out' )
110-
111- self .assertEqual (inn_in , 1 )
112- self .assertEqual (inn_out , 2 )
113-
114- # Another genome splits same connection
115- inn_in2 = tracker .get_innovation_number (0 , 2 , 'add_node_in' )
116- inn_out2 = tracker .get_innovation_number (2 , 1 , 'add_node_out' )
117-
118- # Should get same innovation numbers
119- self .assertEqual (inn_in2 , inn_in )
120- self .assertEqual (inn_out2 , inn_out )
108+ next_node = iter (range (10 , 100 ))
109+
110+ # Genome A splits connection 0->1
111+ node_a , inn_in_a , inn_out_a = tracker .get_node_split (
112+ 0 , 1 , lambda : next (next_node )
113+ )
114+ print (f" Genome A: node={ node_a } , in_inn={ inn_in_a } , out_inn={ inn_out_a } " )
115+
116+ self .assertEqual (node_a , 10 ) # first call allocates from iterator
117+ self .assertEqual (inn_in_a , 1 )
118+ self .assertEqual (inn_out_a , 2 )
119+
120+ # Genome B splits the SAME connection 0->1 in the same generation
121+ node_b , inn_in_b , inn_out_b = tracker .get_node_split (
122+ 0 , 1 , lambda : next (next_node )
123+ )
124+ print (f" Genome B: node={ node_b } , in_inn={ inn_in_b } , out_inn={ inn_out_b } " )
125+
126+ # Must get the same node ID and innovation numbers
127+ self .assertEqual (node_b , node_a )
128+ self .assertEqual (inn_in_b , inn_in_a )
129+ self .assertEqual (inn_out_b , inn_out_a )
130+
131+ def test_node_split_different_connections_get_different_ids (self ):
132+ """Test that splitting different connections produces different node IDs
133+ and innovation numbers."""
134+ tracker = neat .InnovationTracker ()
135+ next_node = iter (range (10 , 100 ))
136+
137+ node_a , inn_in_a , inn_out_a = tracker .get_node_split (
138+ 0 , 1 , lambda : next (next_node )
139+ )
140+ node_b , inn_in_b , inn_out_b = tracker .get_node_split (
141+ 2 , 3 , lambda : next (next_node )
142+ )
143+ print (f" Split (0->1): node={ node_a } , innovations=({ inn_in_a } , { inn_out_a } )" )
144+ print (f" Split (2->3): node={ node_b } , innovations=({ inn_in_b } , { inn_out_b } )" )
145+
146+ self .assertNotEqual (node_a , node_b )
147+ self .assertNotEqual (inn_in_a , inn_in_b )
148+ self .assertNotEqual (inn_out_a , inn_out_b )
149+
150+ def test_node_split_allocator_called_only_once (self ):
151+ """Test that the allocate_node_key callable is only called the first time
152+ a connection is split in a generation (not on dedup hits)."""
153+ tracker = neat .InnovationTracker ()
154+ call_count = [0 ]
155+
156+ def allocator ():
157+ call_count [0 ] += 1
158+ return 42
159+
160+ tracker .get_node_split (0 , 1 , allocator )
161+ self .assertEqual (call_count [0 ], 1 )
162+
163+ # Second call for same split should NOT call allocator
164+ tracker .get_node_split (0 , 1 , allocator )
165+ self .assertEqual (call_count [0 ], 1 )
166+
167+ def test_node_split_reset_between_generations (self ):
168+ """Test that the same connection split in different generations gets
169+ different node IDs and innovation numbers."""
170+ tracker = neat .InnovationTracker ()
171+ next_node = iter (range (10 , 100 ))
172+
173+ node_g1 , inn_in_g1 , inn_out_g1 = tracker .get_node_split (
174+ 0 , 1 , lambda : next (next_node )
175+ )
176+
177+ tracker .reset_generation ()
178+
179+ node_g2 , inn_in_g2 , inn_out_g2 = tracker .get_node_split (
180+ 0 , 1 , lambda : next (next_node )
181+ )
182+ print (f" Gen 1: node={ node_g1 } , innovations=({ inn_in_g1 } , { inn_out_g1 } )" )
183+ print (f" Gen 2: node={ node_g2 } , innovations=({ inn_in_g2 } , { inn_out_g2 } )" )
184+
185+ self .assertNotEqual (node_g1 , node_g2 )
186+ self .assertNotEqual (inn_in_g1 , inn_in_g2 )
187+ self .assertNotEqual (inn_out_g1 , inn_out_g2 )
121188
122189 def test_pickle_serialization (self ):
123190 """Test that InnovationTracker can be pickled and unpickled."""
0 commit comments