99 lines
3.0 KiB
Python
99 lines
3.0 KiB
Python
import numpy as np
|
|
import pytest
|
|
from sklearn.ensemble import RandomForestClassifier
|
|
from sklearn.gaussian_process import GaussianProcessRegressor
|
|
|
|
from lmpm.training.dataset import (
|
|
CATEGORICAL_FEATURE_FIELDS,
|
|
NUMERIC_FEATURE_FIELDS,
|
|
TrainingDataset,
|
|
TrainingRecord,
|
|
)
|
|
from lmpm.training.models import (
|
|
build_estimator,
|
|
load_training_bundle,
|
|
save_training_bundle,
|
|
train_models,
|
|
)
|
|
from lmpm.training.routing import default_model_routes
|
|
|
|
|
|
def make_record(number: int, material_name: str) -> TrainingRecord:
|
|
features = {
|
|
field: float(number + index + 1)
|
|
for index, field in enumerate(NUMERIC_FEATURE_FIELDS)
|
|
}
|
|
features.update(
|
|
{
|
|
CATEGORICAL_FEATURE_FIELDS[0]: "bidirectional",
|
|
CATEGORICAL_FEATURE_FIELDS[1]: "20x20",
|
|
}
|
|
)
|
|
return TrainingRecord(
|
|
metadata={
|
|
"experiment_id": f"LAS-2026-{number:04d}",
|
|
"material_name": material_name,
|
|
},
|
|
features=features,
|
|
targets={
|
|
"is_cut_through": number % 2 == 0,
|
|
"carbonized_edge_width": number / 10,
|
|
"etching_depth": number * 10.0,
|
|
"is_fire_smolder": False,
|
|
"pattern_clarity_score": float(number),
|
|
"presentation_balance_score": float(10 - number),
|
|
},
|
|
)
|
|
|
|
|
|
def training_dataset() -> TrainingDataset:
|
|
return TrainingDataset(
|
|
records=(
|
|
make_record(1, "Acrylic"),
|
|
make_record(2, "Acrylic"),
|
|
make_record(3, "Basswood"),
|
|
make_record(4, "Basswood"),
|
|
)
|
|
)
|
|
|
|
|
|
def test_model_factory_builds_the_routed_estimator_types():
|
|
routes = {route.target_name: route for route in default_model_routes()}
|
|
|
|
assert isinstance(
|
|
build_estimator(routes["etching_depth"]), GaussianProcessRegressor
|
|
)
|
|
assert isinstance(build_estimator(routes["is_cut_through"]), RandomForestClassifier)
|
|
|
|
|
|
def test_training_runs_leakage_safe_lomo_and_persists_bundle(tmp_path):
|
|
bundle = train_models(training_dataset())
|
|
|
|
evaluation = bundle.evaluations["etching_depth"]
|
|
assert [fold.held_out_material for fold in evaluation.folds] == [
|
|
"Acrylic",
|
|
"Basswood",
|
|
]
|
|
assert evaluation.mean_metrics["mae"] is not None
|
|
assert bundle.models["etching_depth"].route.model_family == (
|
|
"gaussian_process_regressor"
|
|
)
|
|
|
|
features = bundle.models["etching_depth"].preprocessor.transform(training_dataset())
|
|
predictions = bundle.models["etching_depth"].estimator.predict(features.values)
|
|
assert isinstance(predictions, np.ndarray)
|
|
assert predictions.shape == (4,)
|
|
|
|
artifact_path = save_training_bundle(bundle, tmp_path / "models.joblib")
|
|
restored = load_training_bundle(artifact_path)
|
|
assert restored.report() == bundle.report()
|
|
|
|
|
|
def test_training_requires_two_distinct_materials():
|
|
dataset = TrainingDataset(
|
|
records=(make_record(1, "Acrylic"), make_record(2, "Acrylic"))
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="at least two materials"):
|
|
train_models(dataset)
|