From 34e0cd34693270c3a11095884ec95657b6271fd2 Mon Sep 17 00:00:00 2001 From: LeoGrin Date: Fri, 7 Nov 2025 15:08:50 +0100 Subject: [PATCH 1/3] directly pass the model path through + simplify the v2 vs v2.5 logic --- tabpfn_client/estimator.py | 186 +++++++----------- .../tests/unit/test_tabpfn_classifier.py | 109 ---------- .../tests/unit/test_tabpfn_regressor.py | 105 ---------- 3 files changed, 73 insertions(+), 327 deletions(-) diff --git a/tabpfn_client/estimator.py b/tabpfn_client/estimator.py index 896b155..b1ab4f9 100644 --- a/tabpfn_client/estimator.py +++ b/tabpfn_client/estimator.py @@ -32,92 +32,25 @@ MAX_COLS = 2000 MAX_NUMBER_OF_CLASSES = 10 -# Special string used to identify v2.5 models in model paths. -V_2_5_IDENTIFIER = "v2.5" -DEFAULT_V2_MODEL_PATH = "v2_default" -DEFAULT_V2_5_MODEL_PATH = "v2.5_default" - - -class TabPFNModelSelection: - """Base class for TabPFN model selection and path handling.""" - - _AVAILABLE_MODELS: list[str] = [] - _VALID_TASKS = {"classification", "regression"} - - @classmethod - def list_available_models(cls) -> list[str]: - return cls._AVAILABLE_MODELS - - @classmethod - def _validate_model_name(cls, model_name: str) -> None: - if model_name != "default" and model_name not in cls._AVAILABLE_MODELS: - raise ValueError( - f"Invalid model name: {model_name}. " - f"Available models are: {cls.list_available_models()}" - ) - - @classmethod - def _model_name_to_path( - cls, task: Literal["classification", "regression"], model_name: str - ) -> Optional[str]: - cls._validate_model_name(model_name) - model_name_task = "classifier" if task == "classification" else "regressor" - # Let the server handle the default model. This enables v2.5 as well. - if model_name == "default": - return None - if V_2_5_IDENTIFIER in model_name: - return f"tabpfn-{V_2_5_IDENTIFIER}-{model_name_task}-{model_name}.ckpt" - return f"tabpfn-v2-{model_name_task}-{model_name}.ckpt" - - @classmethod - def create_default_for_version(cls, version: ModelVersion, **overrides) -> Self: - """Construct an estimator that uses the given version of the model. - - In addition to selecting the model, this also configures the estimator with - certain default settings associated with this model version. - - Any kwargs will override the default settings. - """ - options = { - "n_estimators": 8, - "softmax_temperature": 0.9, - } - if version == ModelVersion.V2: - options["model_path"] = DEFAULT_V2_MODEL_PATH - elif version == ModelVersion.V2_5: - options["model_path"] = DEFAULT_V2_5_MODEL_PATH - else: - raise ValueError(f"Unknown version: {version}") +def _default_model_path_v2(task: Literal["classification", "regression"]) -> str: + if task == "classification": + return "tabpfn-v2-classifier-finetuned-zk73skhh.ckpt" + elif task == "regression": + return "tabpfn-v2-regressor.ckpt" + else: + raise ValueError(f"Invalid task: {task}") - options.update(overrides) - return cls(**options) +def _default_model_path_v2_5(task: Literal["classification", "regression"]) -> str: + # using auto makes this robust to changes in the tabpfn package + return "auto" -class TabPFNClassifier(ClassifierMixin, BaseEstimator, TabPFNModelSelection): - _AVAILABLE_MODELS = [ - "v2.5_default-2", - DEFAULT_V2_5_MODEL_PATH, - "v2.5_large-features-L", - "v2.5_large-features-XL", - "v2.5_large-samples", - "v2.5_real-large-features", - "v2.5_real-large-samples-and-features", - "v2.5_real", - "v2.5_variant", - DEFAULT_V2_MODEL_PATH, - "default", - "gn2p4bpt", - "llderlii", - "od3j1g5m", - "vutqq28w", - "znskzxi4", - ] - +class TabPFNClassifier(ClassifierMixin, BaseEstimator): def __init__( self, - model_path: str = "default", + model_path: str = "auto", n_estimators: int = 8, softmax_temperature: float = 0.9, balance_probabilities: bool = False, @@ -138,8 +71,9 @@ def __init__( Parameters ---------- - model_path: str, default="default" - The name of the model to use. + model_path: str, default="auto" + The name of the model to use. If "auto", default to our latest + default model. n_estimators: int, default=8 The number of estimators in the TabPFN ensemble. We aggregate the predictions of `n_estimators`-many forward passes of TabPFN. Each forward @@ -194,6 +128,30 @@ def __init__( self.last_train_y = None self.last_meta = {} + @classmethod + def create_default_for_version(cls, version: ModelVersion, **overrides) -> Self: + """Construct an estimator that uses the given version of the model. + + In addition to selecting the model, this also configures the estimator with + certain default settings associated with this model version. + + Any kwargs will override the default settings. + """ + options = { + "n_estimators": 8, + "softmax_temperature": 0.9, + } + if version == ModelVersion.V2: + options["model_path"] = _default_model_path_v2("classification") + elif version == ModelVersion.V2_5: + options["model_path"] = _default_model_path_v2_5("classification") + else: + raise ValueError(f"Unknown version: {version}") + + options.update(overrides) + + return cls(**options) + def fit(self, X, y): # assert init() is called init() @@ -204,9 +162,8 @@ def fit(self, X, y): _check_paper_version(self.paper_version, X) estimator_param = self.get_params() - estimator_param["model_path"] = TabPFNClassifier._model_name_to_path( - "classification", self.model_path - ) + estimator_param["model_path"] = self.model_path + if Config.use_server: self.last_train_set_uid = InferenceClient.fit(X, y, config=estimator_param) self.last_train_X = X @@ -247,9 +204,7 @@ def _predict(self, X, output_type): X = _clean_text_features(X) estimator_param = self.get_params() - estimator_param["model_path"] = TabPFNClassifier._model_name_to_path( - "classification", self.model_path - ) + estimator_param["model_path"] = self.model_path result: PredictionResult = InferenceClient.predict( X, @@ -285,26 +240,10 @@ def _validate_targets_and_classes(self, y) -> np.ndarray: ) -class TabPFNRegressor(RegressorMixin, BaseEstimator, TabPFNModelSelection): - _AVAILABLE_MODELS = [ - DEFAULT_V2_5_MODEL_PATH, - "v2.5_low-skew", - "v2.5_quantiles", - "v2.5_real-variant", - "v2.5_real", - "v2.5_small-samples", - "v2.5_variant", - DEFAULT_V2_MODEL_PATH, - "default", - "2noar4o2", - "5wof9ojf", - "09gpqh39", - "wyl4o83o", - ] - +class TabPFNRegressor(RegressorMixin, BaseEstimator): def __init__( self, - model_path: str = "default", + model_path: str = "auto", n_estimators: int = 8, softmax_temperature: float = 0.9, average_before_softmax: bool = False, @@ -324,8 +263,9 @@ def __init__( Parameters ---------- - model_path: str, default="default" - The name to the model to use. + model_path: str, default="auto" + The name to the model to use. If "auto", default to our latest + default model. n_estimators: int, default=8 The number of estimators in the TabPFN ensemble. We aggregate the predictions of `n_estimators`-many forward passes of TabPFN. Each forward @@ -373,6 +313,30 @@ def __init__( self.last_train_y = None self.last_meta = {} + @classmethod + def create_default_for_version(cls, version: ModelVersion, **overrides) -> Self: + """Construct an estimator that uses the given version of the model. + + In addition to selecting the model, this also configures the estimator with + certain default settings associated with this model version. + + Any kwargs will override the default settings. + """ + options = { + "n_estimators": 8, + "softmax_temperature": 0.9, + } + if version == ModelVersion.V2: + options["model_path"] = _default_model_path_v2("regression") + elif version == ModelVersion.V2_5: + options["model_path"] = _default_model_path_v2_5("regression") + else: + raise ValueError(f"Unknown version: {version}") + + options.update(overrides) + + return cls(**options) + def fit(self, X, y): # assert init() is called init() @@ -383,9 +347,7 @@ def fit(self, X, y): _check_paper_version(self.paper_version, X) estimator_param = self.get_params() - estimator_param["model_path"] = TabPFNRegressor._model_name_to_path( - "regression", self.model_path - ) + estimator_param["model_path"] = self.model_path if Config.use_server: self.last_train_set_uid = InferenceClient.fit(X, y, config=estimator_param) self.last_train_X = X @@ -441,9 +403,7 @@ def predict( } estimator_param = self.get_params() - estimator_param["model_path"] = TabPFNRegressor._model_name_to_path( - "regression", self.model_path - ) + estimator_param["model_path"] = self.model_path result: PredictionResult = InferenceClient.predict( X, diff --git a/tabpfn_client/tests/unit/test_tabpfn_classifier.py b/tabpfn_client/tests/unit/test_tabpfn_classifier.py index 61f2614..3909d6a 100644 --- a/tabpfn_client/tests/unit/test_tabpfn_classifier.py +++ b/tabpfn_client/tests/unit/test_tabpfn_classifier.py @@ -765,115 +765,6 @@ def test_too_many_classes_raise_error(self): tabpfn.fit(X, y_str) self.assertIn("exceeds the maximal number of", str(cm.exception)) - -class TestTabPFNModelSelection(unittest.TestCase): - def setUp(self): - # skip init - config.Config.is_initialized = True - config.Config.use_server = True - - def tearDown(self): - # undo setUp - config.reset() - - def test_list_available_models_returns_expected_models(self): - expected_models = [ - "v2.5_default-2", - "v2.5_default", - "v2.5_large-features-L", - "v2.5_large-features-XL", - "v2.5_large-samples", - "v2.5_real-large-features", - "v2.5_real-large-samples-and-features", - "v2.5_real", - "v2.5_variant", - "v2_default", - "default", - "gn2p4bpt", - "llderlii", - "od3j1g5m", - "vutqq28w", - "znskzxi4", - ] - self.assertEqual(TabPFNClassifier.list_available_models(), expected_models) - - def test_model_names_that_are_substrings_come_later(self): - # Mitigation to ensure that model "parsing" in the tabpfn-time-series - # package continues to work. Long-term we should fix that package as - # that "parsing" is quite brittle. - # https://github.com/PriorLabs/tabpfn-time-series/blob/71c22aed9d3f8ec280ffb753d0e87086be3cb7a4/tabpfn_time_series/worker/model_adapters/tabpfn_adapter.py#L18 - - model_names = TabPFNClassifier.list_available_models() - - for i in range(len(model_names)): - possible_substring = model_names[i] - for j in range(i + 1, len(model_names)): - model_name = model_names[j] - self.assertNotIn(possible_substring, model_name) - - def test_validate_model_name_with_valid_model_passes(self): - # Should not raise any exception - TabPFNClassifier._validate_model_name("default") - TabPFNClassifier._validate_model_name("gn2p4bpt") - - def test_validate_model_name_with_invalid_model_raises_error(self): - with self.assertRaises(ValueError): - TabPFNClassifier._validate_model_name("invalid_model") - - def test_model_name_to_path_returns_expected_path(self): - # Test default model path. Server decides. - expected_default_path = None - self.assertEqual( - TabPFNClassifier._model_name_to_path("classification", "default"), - expected_default_path, - ) - - # Test specific model path - expected_specific_path = "tabpfn-v2-classifier-gn2p4bpt.ckpt" - self.assertEqual( - TabPFNClassifier._model_name_to_path("classification", "gn2p4bpt"), - expected_specific_path, - ) - - # Test specific v2.5 model path - expected_specific_path = "tabpfn-v2.5-classifier-v2.5_default.ckpt" - self.assertEqual( - TabPFNClassifier._model_name_to_path("classification", "v2.5_default"), - expected_specific_path, - ) - - def test_model_name_to_path_with_invalid_model_raises_error(self): - with self.assertRaises(ValueError): - TabPFNClassifier._model_name_to_path("classification", "invalid_model") - - def test_predict_proba_uses_correct_model_path(self): - # Setup - X = np.random.rand(10, 5) - y = np.random.randint(0, 2, 10) - - tabpfn = TabPFNClassifier(model_path="gn2p4bpt") - - # Mock the inference client - with patch.object(InferenceClient, "predict") as mock_predict: - mock_predict.return_value = PredictionResult( - y_pred={"probas": np.random.rand(10, 2)}, metadata={} - ) - - with patch.object(InferenceClient, "fit") as mock_fit: - mock_fit.return_value = "dummy_uid" - - # Fit and predict - tabpfn.fit(X, y) - tabpfn.predict_proba(X) - - # Verify the model path was correctly passed to predict - predict_kwargs = mock_predict.call_args[1] - expected_model_path = "tabpfn-v2-classifier-gn2p4bpt.ckpt" - - self.assertEqual( - predict_kwargs["config"]["model_path"], expected_model_path - ) - @patch.object(InferenceClient, "fit", return_value="dummy_uid") @patch.object( InferenceClient, diff --git a/tabpfn_client/tests/unit/test_tabpfn_regressor.py b/tabpfn_client/tests/unit/test_tabpfn_regressor.py index 86d864c..e6848aa 100644 --- a/tabpfn_client/tests/unit/test_tabpfn_regressor.py +++ b/tabpfn_client/tests/unit/test_tabpfn_regressor.py @@ -734,111 +734,6 @@ def test_cross_validation(self, mock_fit, mock_predict): # predict should be called 5 times (once per fold) self.assertEqual(mock_predict.call_count, 5) - -class TestTabPFNModelSelection(unittest.TestCase): - def setUp(self): - # skip init - config.Config.is_initialized = True - config.Config.use_server = True - - def tearDown(self): - # undo setUp - config.reset() - - def test_list_available_models_returns_expected_models(self): - expected_models = [ - "v2.5_default", - "v2.5_low-skew", - "v2.5_quantiles", - "v2.5_real-variant", - "v2.5_real", - "v2.5_small-samples", - "v2.5_variant", - "v2_default", - "default", - "2noar4o2", - "5wof9ojf", - "09gpqh39", - "wyl4o83o", - ] - self.assertEqual(TabPFNRegressor.list_available_models(), expected_models) - - def test_model_names_that_are_substrings_come_later(self): - # Mitigation to ensure that model "parsing" in the tabpfn-time-series - # package continues to work. Long-term we should fix that package as - # that "parsing" is quite brittle. - # https://github.com/PriorLabs/tabpfn-time-series/blob/71c22aed9d3f8ec280ffb753d0e87086be3cb7a4/tabpfn_time_series/worker/model_adapters/tabpfn_adapter.py#L18 - - model_names = TabPFNRegressor.list_available_models() - - for i in range(len(model_names)): - possible_substring = model_names[i] - for j in range(i + 1, len(model_names)): - model_name = model_names[j] - self.assertNotIn(possible_substring, model_name) - - def test_validate_model_name_with_valid_model_passes(self): - # Should not raise any exception - TabPFNRegressor._validate_model_name("default") - TabPFNRegressor._validate_model_name("2noar4o2") - - def test_validate_model_name_with_invalid_model_raises_error(self): - with self.assertRaises(ValueError): - TabPFNRegressor._validate_model_name("invalid_model") - - def test_model_name_to_path_returns_expected_path(self): - # Test default model path. Server decides. - expected_default_path = None - self.assertEqual( - TabPFNRegressor._model_name_to_path("regression", "default"), - expected_default_path, - ) - - # Test specific model path - expected_specific_path = "tabpfn-v2-regressor-2noar4o2.ckpt" - self.assertEqual( - TabPFNRegressor._model_name_to_path("regression", "2noar4o2"), - expected_specific_path, - ) - - # Test specific v2.5 model path - expected_specific_path = "tabpfn-v2.5-regressor-v2.5_default.ckpt" - self.assertEqual( - TabPFNRegressor._model_name_to_path("regression", "v2.5_default"), - expected_specific_path, - ) - - def test_model_name_to_path_with_invalid_model_raises_error(self): - with self.assertRaises(ValueError): - TabPFNRegressor._model_name_to_path("regression", "invalid_model") - - def test_predict_uses_correct_model_path(self): - # Setup - X = np.random.rand(10, 5) - y = np.random.rand(10) - - tabpfn = TabPFNRegressor(model_path="2noar4o2") - - # Mock the inference client - with patch.object(InferenceClient, "predict") as mock_predict: - mock_predict.return_value = PredictionResult( - y_pred={"mean": np.random.rand(10)}, metadata={} - ) - with patch.object(InferenceClient, "fit") as mock_fit: - mock_fit.return_value = "dummy_uid" - - # Fit and predict - tabpfn.fit(X, y) - tabpfn.predict(X) - - # Verify the model path was correctly passed to predict - predict_kwargs = mock_predict.call_args[1] - expected_model_path = "tabpfn-v2-regressor-2noar4o2.ckpt" - - self.assertEqual( - predict_kwargs["config"]["model_path"], expected_model_path - ) - @patch.object(InferenceClient, "fit", return_value="dummy_uid") @patch.object( InferenceClient, From 5eec79e2899432279c401e4cf50fdd4f75faa813 Mon Sep 17 00:00:00 2001 From: LeoGrin Date: Fri, 7 Nov 2025 15:13:33 +0100 Subject: [PATCH 2/3] use None instead of auto to make the server happy --- tabpfn_client/estimator.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tabpfn_client/estimator.py b/tabpfn_client/estimator.py index b1ab4f9..0253533 100644 --- a/tabpfn_client/estimator.py +++ b/tabpfn_client/estimator.py @@ -43,14 +43,14 @@ def _default_model_path_v2(task: Literal["classification", "regression"]) -> str def _default_model_path_v2_5(task: Literal["classification", "regression"]) -> str: - # using auto makes this robust to changes in the tabpfn package - return "auto" + # using None makes this robust to changes in the tabpfn package + return None class TabPFNClassifier(ClassifierMixin, BaseEstimator): def __init__( self, - model_path: str = "auto", + model_path: Optional[str] = None, n_estimators: int = 8, softmax_temperature: float = 0.9, balance_probabilities: bool = False, @@ -71,8 +71,8 @@ def __init__( Parameters ---------- - model_path: str, default="auto" - The name of the model to use. If "auto", default to our latest + model_path: str or None, default=None + The name of the model to use. If None, default to our latest default model. n_estimators: int, default=8 The number of estimators in the TabPFN ensemble. We aggregate the @@ -243,7 +243,7 @@ def _validate_targets_and_classes(self, y) -> np.ndarray: class TabPFNRegressor(RegressorMixin, BaseEstimator): def __init__( self, - model_path: str = "auto", + model_path: Optional[str] = None, n_estimators: int = 8, softmax_temperature: float = 0.9, average_before_softmax: bool = False, @@ -263,8 +263,8 @@ def __init__( Parameters ---------- - model_path: str, default="auto" - The name to the model to use. If "auto", default to our latest + model_path: str or None, default=None + The name to the model to use. If None, default to our latest default model. n_estimators: int, default=8 The number of estimators in the TabPFN ensemble. We aggregate the From fb80d35f303a6b3df0edebb3764ded02d80d5ef6 Mon Sep 17 00:00:00 2001 From: LeoGrin Date: Fri, 7 Nov 2025 15:37:53 +0100 Subject: [PATCH 3/3] add simple tests --- .../tests/unit/test_tabpfn_classifier.py | 62 +++++++++++++++++++ .../tests/unit/test_tabpfn_regressor.py | 58 +++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/tabpfn_client/tests/unit/test_tabpfn_classifier.py b/tabpfn_client/tests/unit/test_tabpfn_classifier.py index 3909d6a..573d7f9 100644 --- a/tabpfn_client/tests/unit/test_tabpfn_classifier.py +++ b/tabpfn_client/tests/unit/test_tabpfn_classifier.py @@ -825,3 +825,65 @@ def test_cross_validation(self, mock_fit, mock_predict): # predict_proba should be called 5 times (once per fold) self.assertEqual(mock_predict.call_count, 5) + + @patch.object(InferenceClient, "fit", return_value="dummy_uid") + def test__model_path_passed_to_fit__model_path_is_passed_to_config(self, mock_fit): + """Test that model_path is passed to the config during fit.""" + init(use_server=True) + + custom_path = "tabpfn-v2-classifier-znskzxi4.ckpt" + classifier = TabPFNClassifier(model_path=custom_path) + + X = np.random.rand(50, 5) + y = np.random.randint(0, 2, 50) + + classifier.fit(X, y) + + # Check that the config passed to fit includes the model_path + actual_config = mock_fit.call_args[1]["config"] + self.assertEqual(actual_config["model_path"], custom_path) + + def test__create_default_for_version_v2__model_path_is_set(self): + """Test create_default_for_version with ModelVersion.V2.""" + from tabpfn_client.constants import ModelVersion + + classifier = TabPFNClassifier.create_default_for_version(ModelVersion.V2) + + # Check that model_path is set to the V2 default + self.assertEqual( + classifier.model_path, "tabpfn-v2-classifier-finetuned-zk73skhh.ckpt" + ) + + def test__create_default_for_version_v2_5__expect_model_path_is_none(self): + """Test create_default_for_version with ModelVersion.V2_5.""" + from tabpfn_client.constants import ModelVersion + + classifier = TabPFNClassifier.create_default_for_version(ModelVersion.V2_5) + + # Check that model_path is None for V2.5 (auto selection) + self.assertIsNone(classifier.model_path) + + def test__create_default_for_version__with_overrides__overrides_are_applied(self): + """Test create_default_for_version with parameter overrides.""" + from tabpfn_client.constants import ModelVersion + + classifier = TabPFNClassifier.create_default_for_version( + ModelVersion.V2, + n_estimators=3, + softmax_temperature=0.7, + balance_probabilities=True, + ) + + # Check that overrides are applied + self.assertEqual(classifier.n_estimators, 3) + self.assertEqual(classifier.softmax_temperature, 0.7) + self.assertEqual(classifier.balance_probabilities, True) + # Model path should still be the default for V2 + self.assertEqual( + classifier.model_path, "tabpfn-v2-classifier-finetuned-zk73skhh.ckpt" + ) + + def test__create_default_for_version__invalid_version__raises_value_error(self): + """Test create_default_for_version with invalid version.""" + with self.assertRaises(ValueError): + TabPFNClassifier.create_default_for_version("invalid_version") diff --git a/tabpfn_client/tests/unit/test_tabpfn_regressor.py b/tabpfn_client/tests/unit/test_tabpfn_regressor.py index e6848aa..eb09073 100644 --- a/tabpfn_client/tests/unit/test_tabpfn_regressor.py +++ b/tabpfn_client/tests/unit/test_tabpfn_regressor.py @@ -759,3 +759,61 @@ def test_paper_version_behavior(self, mock_predict, mock_fit): tabpfn_false.fit(X, y) y_pred_false = tabpfn_false.predict(test_X) self.assertIsNotNone(y_pred_false) + + @patch.object(InferenceClient, "fit", return_value="dummy_uid") + def test__model_path_passed_to_fit__model_path_is_passed_to_config(self, mock_fit): + """Test that model_path is passed to the config during fit.""" + init(use_server=True) + + custom_path = "tabpfn-v2-regressor-tvvss6bp.ckpt" + regressor = TabPFNRegressor(model_path=custom_path) + + X = np.random.rand(50, 5) + y = np.random.rand(50) + + regressor.fit(X, y) + + # Check that the config passed to fit includes the model_path + actual_config = mock_fit.call_args[1]["config"] + self.assertEqual(actual_config["model_path"], custom_path) + + def test__create_default_for_version_v2__model_path_is_set(self): + """Test create_default_for_version with ModelVersion.V2.""" + from tabpfn_client.constants import ModelVersion + + regressor = TabPFNRegressor.create_default_for_version(ModelVersion.V2) + + # Check that model_path is set to the V2 default + self.assertEqual(regressor.model_path, "tabpfn-v2-regressor.ckpt") + + def test__create_default_for_version_v2_5__expect_model_path_is_none(self): + """Test create_default_for_version with ModelVersion.V2_5.""" + from tabpfn_client.constants import ModelVersion + + regressor = TabPFNRegressor.create_default_for_version(ModelVersion.V2_5) + + # Check that model_path is None for V2.5 (auto selection) + self.assertIsNone(regressor.model_path) + + def test__create_default_for_version__with_overrides__overrides_are_applied(self): + """Test create_default_for_version with parameter overrides.""" + from tabpfn_client.constants import ModelVersion + + regressor = TabPFNRegressor.create_default_for_version( + ModelVersion.V2, + n_estimators=3, + softmax_temperature=0.7, + average_before_softmax=True, + ) + + # Check that overrides are applied + self.assertEqual(regressor.n_estimators, 3) + self.assertEqual(regressor.softmax_temperature, 0.7) + self.assertEqual(regressor.average_before_softmax, True) + # Model path should still be the default for V2 + self.assertEqual(regressor.model_path, "tabpfn-v2-regressor.ckpt") + + def test__create_default_for_version__invalid_version__raises_value_error(self): + """Test create_default_for_version with invalid version.""" + with self.assertRaises(ValueError): + TabPFNRegressor.create_default_for_version("invalid_version")