@@ -122,9 +122,10 @@ def test_standard_heterogeneous_mtgp(self) -> None:
122122 model .likelihood .noise_covar .noise .shape [- 1 ], model .num_tasks
123123 )
124124
125- # Evaluate the posterior.
125+ # Evaluate the posterior (task column required) .
126126 with self .assertRaisesRegex (UnsupportedError , "output_indices" ):
127127 model .posterior (self .ds1 .X , output_indices = [0 , 1 ])
128+ # ds1.X already has task column (last col = 0)
128129 posterior = model .posterior (self .ds1 .X )
129130 self .assertIsInstance (posterior , GPyTorchPosterior )
130131 self .assertIsInstance (posterior .distribution , MultivariateNormal )
@@ -191,7 +192,7 @@ def test_identical_search_space(self) -> None:
191192 self .assertEqual (model .train_inputs [0 ].shape , torch .Size ([8 , 4 ]))
192193 data_covar_module = model .covar_module .kernels [0 ]
193194 self .assertEqual (len (data_covar_module .kernels ), 1 )
194- # Evaluate the posterior.
195+ # Evaluate the posterior (ds1.X has task col = 0) .
195196 posterior = model .posterior (self .ds1 .X )
196197 self .assertEqual (posterior .mean .shape , torch .Size ([5 , 1 ]))
197198 posterior = model .posterior (self .ds1 .X .repeat (3 , 1 , 1 ))
@@ -243,10 +244,82 @@ def test_with_no_target_data(self) -> None:
243244 model .forward (model .map_to_full_tensor (X = torch .zeros (5 , 3 ), task_index = 0 ))
244245 # Evaluation with task 2 -- requires all_tasks to be passed in to the model.
245246 model .forward (model .map_to_full_tensor (X = torch .zeros (5 , 4 ), task_index = 2 ))
246- # Evaluate the posterior.
247- posterior = model .posterior (torch .rand (5 , 3 ))
247+ # Evaluate the posterior (task column required).
248+ X_with_task = torch .cat ([torch .rand (5 , 3 ), torch .zeros (5 , 1 )], dim = - 1 )
249+ posterior = model .posterior (X_with_task )
248250 self .assertIsInstance (posterior , GPyTorchPosterior )
249251 self .assertIsInstance (posterior .mvn , MultivariateNormal )
250252 self .assertEqual (posterior .mean .shape , torch .Size ([5 , 1 ]))
251- posterior = model .posterior (torch .rand (3 , 5 , 3 ))
253+ X_batch_with_task = torch .cat (
254+ [torch .rand (3 , 5 , 3 ), torch .zeros (3 , 5 , 1 )], dim = - 1
255+ )
256+ posterior = model .posterior (X_batch_with_task )
252257 self .assertEqual (posterior .mean .shape , torch .Size ([3 , 5 , 1 ]))
258+
259+ def test_feature_ordering_preserves_target_order (self ) -> None :
260+ """Test that construct_inputs uses target's feature order as canonical."""
261+ # Create target dataset with features in order: A, B, C
262+ target_ds = SupervisedDataset (
263+ X = torch .cat ([torch .rand (3 , 3 ), torch .zeros (3 , 1 )], dim = - 1 ),
264+ Y = torch .rand (3 , 1 ),
265+ feature_names = ["A" , "B" , "C" , "task" ],
266+ outcome_names = ["target" ],
267+ )
268+ # Create source dataset with features in different order: C, A, B
269+ source_ds = SupervisedDataset (
270+ X = torch .cat ([torch .rand (2 , 3 ), torch .ones (2 , 1 )], dim = - 1 ),
271+ Y = torch .rand (2 , 1 ),
272+ feature_names = ["C" , "A" , "B" , "task" ],
273+ outcome_names = ["source" ],
274+ )
275+ mtds = MultiTaskDataset (
276+ datasets = [target_ds , source_ds ],
277+ target_outcome_name = "target" ,
278+ task_feature_index = - 1 ,
279+ )
280+ model_inputs = HeterogeneousMTGP .construct_inputs (training_data = mtds )
281+
282+ with self .subTest ("feature_indices_preserve_target_order" ):
283+ # Target: A, B, C -> canonical [0, 1, 2]
284+ # Source: C, A, B -> maps to [2, 0, 1] in canonical order
285+ self .assertEqual (model_inputs ["feature_indices" ], [[0 , 1 , 2 ], [2 , 0 , 1 ]])
286+
287+ with self .subTest ("source_only_features_appended_at_end" ):
288+ # Target: A, B; Source: B, C -> canonical should be [A, B, C]
289+ target_ds2 = SupervisedDataset (
290+ X = torch .cat ([torch .rand (3 , 2 ), torch .zeros (3 , 1 )], dim = - 1 ),
291+ Y = torch .rand (3 , 1 ),
292+ feature_names = ["A" , "B" , "task" ],
293+ outcome_names = ["target" ],
294+ )
295+ source_ds2 = SupervisedDataset (
296+ X = torch .cat ([torch .rand (2 , 2 ), torch .ones (2 , 1 )], dim = - 1 ),
297+ Y = torch .rand (2 , 1 ),
298+ feature_names = ["B" , "C" , "task" ],
299+ outcome_names = ["source" ],
300+ )
301+ mtds2 = MultiTaskDataset (
302+ datasets = [target_ds2 , source_ds2 ],
303+ target_outcome_name = "target" ,
304+ task_feature_index = - 1 ,
305+ )
306+ model_inputs2 = HeterogeneousMTGP .construct_inputs (training_data = mtds2 )
307+ # Target: A, B -> [0, 1]; Source: B, C -> [1, 2]
308+ self .assertEqual (model_inputs2 ["feature_indices" ], [[0 , 1 ], [1 , 2 ]])
309+ self .assertEqual (model_inputs2 ["full_feature_dim" ], 3 )
310+
311+ def test_posterior_requires_task_column (self ) -> None :
312+ """Test that posterior rejects X without task column."""
313+ model_inputs = HeterogeneousMTGP .construct_inputs (training_data = self .mtds )
314+ model = HeterogeneousMTGP (** model_inputs )
315+ model .eval ()
316+ # d_target=3, so posterior requires exactly 4 columns
317+
318+ with self .subTest ("rejects_no_task_column" ):
319+ with self .assertRaisesRegex (ValueError , "Expected X with 4 columns" ):
320+ model .posterior (torch .rand (4 , 3 ))
321+
322+ with self .subTest ("rejects_full_space" ):
323+ X_full = torch .cat ([torch .rand (4 , 5 ), torch .zeros (4 , 1 )], dim = - 1 )
324+ with self .assertRaisesRegex (ValueError , "Expected X with 4 columns" ):
325+ model .posterior (X_full )
0 commit comments