1+ import time
2+ import unittest
3+ import torch
4+ from torch import nn
5+
6+ from antsnormflows .utils .splines import search_sorted
7+ from antsnormflows .flows .affine .glow import GlowBlock3d
8+ from antsnormflows .core import ConditionalNormalizingFlow
9+ from antsnormflows .distributions .base import DiagGaussian , ConditionalDiagGaussian
10+ from antsnormflows .flows .neural_spline .wrapper import AutoregressiveRationalQuadraticSpline
11+
12+ class TestSearchsortedCorrectness (unittest .TestCase ):
13+ def test_binary_search_correctness (self ):
14+ """Vérifie que search_sorted retourne les bons indices (Fix GB-4)."""
15+ bin_locations = torch .tensor ([[0.0 , 0.25 , 0.5 , 0.75 , 1.0 ]])
16+ inputs = torch .tensor ([[0.0 , 0.1 , 0.3 , 0.6 , 0.99 ]])
17+ expected = torch .tensor ([[0 , 0 , 1 , 2 , 3 ]])
18+
19+ result = search_sorted (bin_locations , inputs )
20+ torch .testing .assert_close (result , expected )
21+
22+ def test_edge_cases (self ):
23+ """Test des cas limites (valeurs exactement sur les bornes)."""
24+ bin_locations = torch .tensor ([[0.0 , 0.5 , 1.0 ]])
25+ inputs = torch .tensor ([[1.0 ]])
26+ result = search_sorted (bin_locations , inputs )
27+ self .assertEqual (result .item (), 1 )
28+
29+
30+ class TestNeuroImagingIntegration (unittest .TestCase ):
31+ """Tests spécifiques aux cas d'usage neuro-imagerie 3D."""
32+
33+ def test_glow_block_3d_forward_inverse (self ):
34+ """Teste GlowBlock3d sur un volume IRM synthétique miniature."""
35+ torch .manual_seed (42 )
36+ C , D , H , W = 2 , 16 , 16 , 16
37+
38+ block = GlowBlock3d (
39+ channels = C ,
40+ hidden_channels = 8 ,
41+ split_mode = 'channel' ,
42+ scale = True
43+ )
44+
45+ x = torch .randn (2 , C , D , H , W )
46+ x_fwd , log_det_fwd = block (x )
47+ x_rec , log_det_inv = block .inverse (x_fwd )
48+
49+ # Tolérance augmentée pour le float32
50+ torch .testing .assert_close (x_rec , x , atol = 1e-4 , rtol = 1e-4 )
51+ torch .testing .assert_close (log_det_fwd + log_det_inv ,
52+ torch .zeros_like (log_det_fwd ), atol = 1e-4 , rtol = 1e-4 )
53+
54+ def test_conditional_flow_on_morphometric_features (self ):
55+ """Simule un conditionnement sur l'âge/sexe du patient."""
56+ torch .manual_seed (42 )
57+ n_dims = 10
58+ n_context = 2
59+ n_samples = 50
60+
61+ layer = AutoregressiveRationalQuadraticSpline (
62+ num_input_channels = n_dims ,
63+ num_blocks = 2 ,
64+ num_hidden_channels = 32 ,
65+ num_context_channels = n_context
66+ )
67+ base = DiagGaussian (n_dims )
68+
69+ # Ajout de la forme et de l'encodeur de contexte (mapping N -> 2 * D)
70+ context_encoder = torch .nn .Linear (n_context , 2 * n_dims )
71+ target = ConditionalDiagGaussian (shape = (n_dims ,), context_encoder = context_encoder )
72+
73+ model = ConditionalNormalizingFlow (base , [layer ], target )
74+
75+ x = torch .randn (n_samples , n_dims )
76+ context = torch .randn (n_samples , n_context )
77+
78+ log_p = model .log_prob (x , context = context )
79+
80+ self .assertEqual (log_p .shape , (n_samples ,))
81+ self .assertFalse (torch .isnan (log_p ).any ())
82+ self .assertFalse (torch .isinf (log_p ).any ())
83+
84+ class TestPerformanceRegression (unittest .TestCase ):
85+ """Tests de non-régression de performance sur GPU."""
86+
87+ @classmethod
88+ def setUpClass (cls ):
89+ if not torch .cuda .is_available ():
90+ raise unittest .SkipTest ("Tests de performance ignorés : GPU requis" )
91+ cls .device = torch .device ("cuda:0" )
92+
93+ def test_nsf_memory_footprint (self ):
94+ """Vérifie la croissance linéaire de la mémoire avec le batch size."""
95+ n_dims = 8
96+ layer = AutoregressiveRationalQuadraticSpline (
97+ num_input_channels = n_dims , num_blocks = 2 , num_hidden_channels = 32
98+ ).to (self .device )
99+
100+ # Test petit batch
101+ torch .cuda .reset_peak_memory_stats ()
102+ z_small = torch .randn (64 , n_dims , device = self .device )
103+ _ = layer (z_small )
104+ mem_small = torch .cuda .max_memory_allocated () / 1e6
105+
106+ # Test grand batch
107+ torch .cuda .reset_peak_memory_stats ()
108+ z_large = torch .randn (256 , n_dims , device = self .device )
109+ _ = layer (z_large )
110+ mem_large = torch .cuda .max_memory_allocated () / 1e6
111+
112+ memory_ratio = mem_large / mem_small if mem_small > 0 else 1.0
113+ batch_ratio = 256 / 64 # = 4.0
114+
115+ # Vérifie que la mémoire ne croît pas de manière quadratique
116+ self .assertLess (memory_ratio , batch_ratio * 1.5 )
0 commit comments