@@ -165,3 +165,204 @@ def test_version_check_dropout_available():
165165 assert len (cebra .models .get_options ("*dropout*" )) == 0
166166 else :
167167 assert len (cebra .models .get_options ("*dropout*" )) > 0
168+
169+
170+ # Tests for parametrized offset models backward compatibility
171+ from _reference_implementations import Offset5ModelReference
172+ from _reference_implementations import Offset10ModelReference
173+ from _reference_implementations import Offset15ModelReference
174+ from _reference_implementations import Offset20ModelReference
175+ from _reference_implementations import Offset36Reference
176+ from _reference_implementations import Offset40Reference
177+ from _reference_implementations import Offset50Reference
178+
179+
180+ @pytest .mark .parametrize ("offset_n,reference_class" , [
181+ (5 , Offset5ModelReference ),
182+ (10 , Offset10ModelReference ),
183+ (15 , Offset15ModelReference ),
184+ (20 , Offset20ModelReference ),
185+ (36 , Offset36Reference ),
186+ (40 , Offset40Reference ),
187+ (50 , Offset50Reference ),
188+ ])
189+ def test_parametrized_offset_models_match_reference (offset_n , reference_class ):
190+ """Test that parametrized offset models produce identical output to reference hardcoded models."""
191+
192+ num_neurons = 5
193+ num_units = 8
194+ num_output = 3
195+ normalize = True
196+
197+ # Create reference model
198+ ref_model = reference_class (num_neurons ,
199+ num_units ,
200+ num_output ,
201+ normalize = normalize )
202+
203+ # Create parametrized model using OffsetNModel
204+ param_model = cebra .models .init (f"offset{ offset_n } -model" ,
205+ num_neurons = num_neurons ,
206+ num_units = num_units ,
207+ num_output = num_output )
208+
209+ # Test 1: Check offsets match
210+ ref_offset = ref_model .get_offset ()
211+ param_offset = param_model .get_offset ()
212+ assert ref_offset .left == param_offset .left , \
213+ f"Offset left mismatch for offset{ offset_n } : { ref_offset .left } != { param_offset .left } "
214+ assert ref_offset .right == param_offset .right , \
215+ f"Offset right mismatch for offset{ offset_n } : { ref_offset .right } != { param_offset .right } "
216+
217+ # Test 2: Check model architecture - same number of parameters
218+ ref_params = sum (p .numel () for p in ref_model .parameters ())
219+ param_params = sum (p .numel () for p in param_model .parameters ())
220+ assert ref_params == param_params , \
221+ f"Parameter count mismatch for offset{ offset_n } : { ref_params } != { param_params } "
222+
223+ # Test 3: Check output shape consistency
224+ batch_size = 2
225+ input_length = 100
226+ offset_len = len (ref_offset )
227+
228+ test_input = torch .randn (batch_size , num_neurons , offset_len )
229+
230+ with torch .no_grad ():
231+ ref_output = ref_model .net (test_input )
232+ param_output = param_model .net (test_input )
233+
234+ assert ref_output .shape == param_output .shape , \
235+ f"Output shape mismatch for offset{ offset_n } : { ref_output .shape } != { param_output .shape } "
236+
237+ # Test 4: For convolutional models, test on full length input
238+ if isinstance (param_model , cebra .models .ConvolutionalModelMixin ):
239+ test_input_full = torch .randn (batch_size , num_neurons , input_length )
240+
241+ with torch .no_grad ():
242+ ref_output_full = ref_model .net (test_input_full )
243+ param_output_full = param_model .net (test_input_full )
244+
245+ expected_length = input_length - len (ref_offset ) + 1
246+ assert ref_output_full .shape == (batch_size , num_output , expected_length ), \
247+ f"Reference model output shape unexpected for offset{ offset_n } "
248+ assert param_output_full .shape == (batch_size , num_output , expected_length ), \
249+ f"Parametrized model output shape unexpected for offset{ offset_n } "
250+
251+
252+ @pytest .mark .parametrize ("offset_n" , [5 , 10 , 15 , 18 , 20 , 31 , 36 , 40 , 50 ])
253+ def test_parametrized_offset_models_exist (offset_n ):
254+ """Test that all parametrized offset models can be instantiated."""
255+ model = cebra .models .init (f"offset{ offset_n } -model" ,
256+ num_neurons = 5 ,
257+ num_units = 4 ,
258+ num_output = 3 )
259+ assert isinstance (model , cebra .models .Model )
260+ assert isinstance (model , cebra .models .HasFeatureEncoder )
261+ assert isinstance (model , cebra .models .ConvolutionalModelMixin )
262+
263+
264+ @pytest .mark .parametrize ("offset_n,reference_class" , [
265+ (5 , Offset5ModelReference ),
266+ (10 , Offset10ModelReference ),
267+ (15 , Offset15ModelReference ),
268+ (20 , Offset20ModelReference ),
269+ (36 , Offset36Reference ),
270+ (40 , Offset40Reference ),
271+ (50 , Offset50Reference ),
272+ ])
273+ def test_parametrized_offset_models_forward_pass_identical (
274+ offset_n , reference_class ):
275+ """Test that parametrized and reference models produce identical forward pass outputs.
276+
277+ This test verifies that when both models are initialized with the same seed and weights,
278+ they produce identical outputs.
279+ """
280+
281+ num_neurons = 5
282+ num_units = 8
283+ num_output = 3
284+ normalize = True
285+ batch_size = 2
286+
287+ # Set seed for reproducibility
288+ torch .manual_seed (42 )
289+
290+ # Create reference model and get its state dict
291+ ref_model = reference_class (num_neurons ,
292+ num_units ,
293+ num_output ,
294+ normalize = normalize )
295+ ref_state_dict = {k : v .clone () for k , v in ref_model .state_dict ().items ()}
296+
297+ # Create parametrized model
298+ param_model = cebra .models .init (f"offset{ offset_n } -model" ,
299+ num_neurons = num_neurons ,
300+ num_units = num_units ,
301+ num_output = num_output )
302+
303+ # Load the same weights into parametrized model
304+ param_model .load_state_dict (ref_state_dict )
305+
306+ # Test with multiple input sizes
307+ offset = ref_model .get_offset ()
308+ offset_len = len (offset )
309+
310+ for input_length in [offset_len , offset_len * 2 , 100 ]:
311+ test_input = torch .randn (batch_size , num_neurons , input_length )
312+
313+ with torch .no_grad ():
314+ ref_output = ref_model .net (test_input )
315+ param_output = param_model .net (test_input )
316+
317+ # Check that outputs are identical
318+ assert torch .allclose (ref_output , param_output , rtol = 1e-5 , atol = 1e-7 ), \
319+ f"Output mismatch for offset{ offset_n } with input_length={ input_length } "
320+
321+ # Check that outputs have same device and dtype
322+ assert ref_output .device == param_output .device , \
323+ f"Device mismatch for offset{ offset_n } "
324+ assert ref_output .dtype == param_output .dtype , \
325+ f"Dtype mismatch for offset{ offset_n } "
326+
327+
328+ @pytest .mark .parametrize ("offset_n" , [5 , 10 , 15 , 18 , 20 , 31 , 36 , 40 , 50 ])
329+ def test_parametrized_offset_models_layer_structure (offset_n ):
330+ """Test that parametrized models have the correct layer structure."""
331+ num_neurons = 4
332+ num_units = 8
333+ num_output = 3
334+
335+ model = cebra .models .init (f"offset{ offset_n } -model" ,
336+ num_neurons = num_neurons ,
337+ num_units = num_units ,
338+ num_output = num_output )
339+
340+ # Model should have Conv1d -> GELU -> Skip layers -> Conv1d structure
341+ # Extract the actual network layers
342+ layers = list (model .net .children ())
343+
344+ # First layer should be Conv1d
345+ assert isinstance (layers [0 ], nn .Conv1d ), \
346+ f"First layer of offset{ offset_n } model should be Conv1d"
347+ assert layers [0 ].in_channels == num_neurons
348+ assert layers [0 ].out_channels == num_units
349+ assert layers [0 ].kernel_size == (2 ,)
350+
351+ # Last meaningful layer (before Norm and Squeeze) should be Conv1d
352+ # Find the second-to-last Conv1d layer
353+ conv_layers = [l for l in layers if isinstance (l , nn .Conv1d )]
354+ assert len (conv_layers ) >= 2 , \
355+ f"offset{ offset_n } model should have at least 2 Conv1d layers"
356+
357+ last_conv = conv_layers [- 1 ]
358+ assert last_conv .out_channels == num_output
359+
360+ # Check that offset is computed correctly
361+ offset = model .get_offset ()
362+ expected_left = offset_n // 2
363+ expected_right = offset_n // 2 + offset_n % 2
364+
365+ assert offset .left == expected_left , \
366+ f"Offset left for offset{ offset_n } should be { expected_left } , got { offset .left } "
367+ assert offset .right == expected_right , \
368+ f"Offset right for offset{ offset_n } should be { expected_right } , got { offset .right } "
0 commit comments