Skip to content

Commit dbae49a

Browse files
committed
Add deprecation tests on offsets models
1 parent 2d48ef8 commit dbae49a

2 files changed

Lines changed: 364 additions & 0 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""Reference implementations of previously hardcoded offset models before parametrization.
2+
3+
These models are used to verify that the parametrized versions produce identical outputs.
4+
"""
5+
6+
import torch
7+
from torch import nn
8+
9+
import cebra.data
10+
import cebra.data.datatypes
11+
import cebra.models.layers as cebra_layers
12+
from cebra.models.model import _OffsetModel
13+
from cebra.models.model import ConvolutionalModelMixin
14+
15+
16+
class Offset10ModelReference(_OffsetModel, ConvolutionalModelMixin):
17+
"""Reference: CEBRA model with a 10 sample receptive field (offset10-model)."""
18+
19+
def __init__(self, num_neurons, num_units, num_output, normalize=True):
20+
if num_units < 1:
21+
raise ValueError(
22+
f"Hidden dimension needs to be at least 1, but got {num_units}."
23+
)
24+
super().__init__(
25+
nn.Conv1d(num_neurons, num_units, 2),
26+
nn.GELU(),
27+
*self._make_layers(num_units, num_layers=3),
28+
nn.Conv1d(num_units, num_output, 3),
29+
num_input=num_neurons,
30+
num_output=num_output,
31+
normalize=normalize,
32+
)
33+
34+
def get_offset(self) -> cebra.data.datatypes.Offset:
35+
return cebra.data.Offset(5, 5)
36+
37+
38+
class Offset5ModelReference(_OffsetModel, ConvolutionalModelMixin):
39+
"""Reference: CEBRA model with a 5 sample receptive field (offset5-model)."""
40+
41+
def __init__(self, num_neurons, num_units, num_output, normalize=True):
42+
super().__init__(
43+
nn.Conv1d(num_neurons, num_units, 2),
44+
nn.GELU(),
45+
cebra_layers._Skip(nn.Conv1d(num_units, num_units, 3), nn.GELU()),
46+
nn.Conv1d(num_units, num_output, 2),
47+
num_input=num_neurons,
48+
num_output=num_output,
49+
normalize=normalize,
50+
)
51+
52+
def get_offset(self) -> cebra.data.datatypes.Offset:
53+
return cebra.data.Offset(2, 3)
54+
55+
56+
class Offset15ModelReference(_OffsetModel, ConvolutionalModelMixin):
57+
"""Reference: CEBRA model with a 15 sample receptive field (offset15-model)."""
58+
59+
def __init__(self, num_neurons, num_units, num_output, normalize=True):
60+
if num_units < 1:
61+
raise ValueError(
62+
f"Hidden dimension needs to be at least 1, but got {num_units}."
63+
)
64+
super().__init__(
65+
nn.Conv1d(num_neurons, num_units, 2),
66+
nn.GELU(),
67+
*self._make_layers(num_units, num_layers=6),
68+
nn.Conv1d(num_units, num_output, 2),
69+
num_input=num_neurons,
70+
num_output=num_output,
71+
normalize=normalize,
72+
)
73+
74+
def get_offset(self) -> cebra.data.datatypes.Offset:
75+
return cebra.data.Offset(7, 8)
76+
77+
78+
class Offset20ModelReference(_OffsetModel, ConvolutionalModelMixin):
79+
"""Reference: CEBRA model with a 20 sample receptive field (offset20-model)."""
80+
81+
def __init__(self, num_neurons, num_units, num_output, normalize=True):
82+
if num_units < 1:
83+
raise ValueError(
84+
f"Hidden dimension needs to be at least 1, but got {num_units}."
85+
)
86+
super().__init__(
87+
nn.Conv1d(num_neurons, num_units, 2),
88+
nn.GELU(),
89+
*self._make_layers(num_units, num_layers=8),
90+
nn.Conv1d(num_units, num_output, 3),
91+
num_input=num_neurons,
92+
num_output=num_output,
93+
normalize=normalize,
94+
)
95+
96+
def get_offset(self) -> cebra.data.datatypes.Offset:
97+
return cebra.data.Offset(10, 10)
98+
99+
100+
class Offset36Reference(_OffsetModel, ConvolutionalModelMixin):
101+
"""Reference: CEBRA model with a 36 sample receptive field (offset36-model)."""
102+
103+
def __init__(self, num_neurons, num_units, num_output, normalize=True):
104+
if num_units < 1:
105+
raise ValueError(
106+
f"Hidden dimension needs to be at least 1, but got {num_units}."
107+
)
108+
super().__init__(
109+
nn.Conv1d(num_neurons, num_units, 2),
110+
nn.GELU(),
111+
*self._make_layers(num_units, num_layers=16),
112+
nn.Conv1d(num_units, num_output, 3),
113+
num_input=num_neurons,
114+
num_output=num_output,
115+
normalize=normalize,
116+
)
117+
118+
def get_offset(self) -> cebra.data.datatypes.Offset:
119+
return cebra.data.Offset(18, 18)
120+
121+
122+
class Offset40Reference(_OffsetModel, ConvolutionalModelMixin):
123+
"""Reference: CEBRA model with a 40 sample receptive field (offset40-model)."""
124+
125+
def __init__(self, num_neurons, num_units, num_output, normalize=True):
126+
if num_units < 1:
127+
raise ValueError(
128+
f"Hidden dimension needs to be at least 1, but got {num_units}."
129+
)
130+
super().__init__(
131+
nn.Conv1d(num_neurons, num_units, 2),
132+
nn.GELU(),
133+
*self._make_layers(num_units, 18),
134+
nn.Conv1d(num_units, num_output, 3),
135+
num_input=num_neurons,
136+
num_output=num_output,
137+
normalize=normalize,
138+
)
139+
140+
def get_offset(self) -> cebra.data.datatypes.Offset:
141+
return cebra.data.Offset(20, 20)
142+
143+
144+
class Offset50Reference(_OffsetModel, ConvolutionalModelMixin):
145+
"""Reference: CEBRA model with a 50 sample receptive field (offset50-model)."""
146+
147+
def __init__(self, num_neurons, num_units, num_output, normalize=True):
148+
if num_units < 1:
149+
raise ValueError(
150+
f"Hidden dimension needs to be at least 1, but got {num_units}."
151+
)
152+
super().__init__(
153+
nn.Conv1d(num_neurons, num_units, 2),
154+
nn.GELU(),
155+
*self._make_layers(num_units, 23),
156+
nn.Conv1d(num_units, num_output, 3),
157+
num_input=num_neurons,
158+
num_output=num_output,
159+
normalize=normalize,
160+
)
161+
162+
def get_offset(self) -> cebra.data.datatypes.Offset:
163+
return cebra.data.Offset(25, 25)

tests/test_models.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)