initial commit FESurrogateModelTutorial
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from femsurrogate.fea.assembly import assemble_global_stiffness, constrained_dofs
|
||||
from femsurrogate.fea.io import read_beam_example
|
||||
from femsurrogate.fea.solver import solve_linear_static
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_assemble_global_stiffness_for_cantilever_fixture_has_expected_shape():
|
||||
model = read_beam_example(ROOT / "BeamExamples" / "CantileverBeam.txt")
|
||||
stiffness = assemble_global_stiffness(model)
|
||||
|
||||
assert stiffness.shape == (18, 18)
|
||||
|
||||
|
||||
def test_fixed_node_dofs_are_constrained():
|
||||
model = read_beam_example(ROOT / "BeamExamples" / "CantileverBeam.txt")
|
||||
|
||||
assert constrained_dofs(model) == (0, 1, 2)
|
||||
|
||||
|
||||
def test_solve_linear_static_returns_finite_displacements_for_all_nodes():
|
||||
model = read_beam_example(ROOT / "BeamExamples" / "CantileverBeam.txt")
|
||||
displacements = solve_linear_static(model)
|
||||
|
||||
assert set(displacements) == set(model.nodes)
|
||||
values = np.array([[d.ux, d.uy, d.rz] for d in displacements.values()])
|
||||
assert np.isfinite(values).all()
|
||||
@@ -0,0 +1,47 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from femsurrogate.fea.io import read_beam_example, read_expected_displacements
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_read_beam_example_parses_cantilever_fixture():
|
||||
model = read_beam_example(ROOT / "BeamExamples" / "CantileverBeam.txt")
|
||||
|
||||
assert model.metadata["Area"] == pytest.approx(1.0)
|
||||
assert model.metadata["Izz"] == pytest.approx(0.0833333)
|
||||
assert model.metadata["ElasticModulus"] == pytest.approx(2.05e11)
|
||||
assert model.metadata["Poisson'sRatio"] == pytest.approx(0.3)
|
||||
|
||||
assert len(model.nodes) == 6
|
||||
assert model.nodes[1].x == pytest.approx(0.0)
|
||||
assert model.nodes[6].x == pytest.approx(5.0)
|
||||
assert model.nodes[6].y == pytest.approx(0.0)
|
||||
|
||||
assert len(model.beams) == 5
|
||||
assert model.beams[0].id == 1
|
||||
assert model.beams[0].node_i == 1
|
||||
assert model.beams[0].node_j == 2
|
||||
assert model.beams[-1].node_i == 5
|
||||
assert model.beams[-1].node_j == 6
|
||||
|
||||
assert model.fixed_nodes == (1,)
|
||||
assert model.nodal_loads[6].fx == pytest.approx(0.0)
|
||||
assert model.nodal_loads[6].fy == pytest.approx(-100000.0)
|
||||
assert model.nodal_loads[6].mz == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_read_expected_displacements_parses_reference_values():
|
||||
displacements = read_expected_displacements(
|
||||
ROOT / "BeamExamples" / "CantileverBeam_Displacements.txt"
|
||||
)
|
||||
|
||||
assert len(displacements) == 6
|
||||
assert displacements[1].ux == pytest.approx(0.0)
|
||||
assert displacements[1].uy == pytest.approx(0.0)
|
||||
assert displacements[1].rz == pytest.approx(0.0)
|
||||
assert displacements[6].ux == pytest.approx(0.0)
|
||||
assert displacements[6].uy == pytest.approx(-0.000244)
|
||||
assert displacements[6].rz == pytest.approx(0.000073)
|
||||
@@ -0,0 +1,42 @@
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from femsurrogate.fea.benchmark import cantilever_tip_displacement
|
||||
from femsurrogate.fea.io import read_beam_example, read_expected_displacements
|
||||
from femsurrogate.fea.solver import solve_linear_static
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INPUT_PATH = ROOT / "BeamExamples" / "CantileverBeam.txt"
|
||||
EXPECTED_PATH = ROOT / "BeamExamples" / "CantileverBeam_Displacements.txt"
|
||||
|
||||
|
||||
def test_solver_matches_cantilever_fixture_displacements():
|
||||
model = read_beam_example(INPUT_PATH)
|
||||
actual = solve_linear_static(model)
|
||||
expected = read_expected_displacements(EXPECTED_PATH)
|
||||
|
||||
assert set(actual) == set(expected)
|
||||
for node_id, expected_displacement in expected.items():
|
||||
actual_displacement = actual[node_id]
|
||||
np.testing.assert_allclose(
|
||||
[actual_displacement.ux, actual_displacement.uy, actual_displacement.rz],
|
||||
[expected_displacement.ux, expected_displacement.uy, expected_displacement.rz],
|
||||
atol=5e-7,
|
||||
rtol=1e-6,
|
||||
)
|
||||
|
||||
|
||||
def test_tip_displacement_matches_analytical_cantilever_magnitude():
|
||||
model = read_beam_example(INPUT_PATH)
|
||||
actual_tip_uy = solve_linear_static(model)[6].uy
|
||||
node_x = [node.x for node in model.nodes.values()]
|
||||
length = max(node_x) - min(node_x)
|
||||
analytical_tip_uy = cantilever_tip_displacement(
|
||||
force_y=model.nodal_loads[6].fy,
|
||||
length=length,
|
||||
E=model.metadata["ElasticModulus"],
|
||||
Izz=model.metadata["Izz"],
|
||||
)
|
||||
|
||||
np.testing.assert_allclose(abs(actual_tip_uy), abs(analytical_tip_uy), rtol=1e-9, atol=0.0)
|
||||
@@ -0,0 +1,27 @@
|
||||
import pandas as pd
|
||||
|
||||
from femsurrogate.data.bounds import DEFAULT_PARAMETER_BOUNDS
|
||||
from femsurrogate.data.dataset import build_dataset, run_beam2d_case
|
||||
from femsurrogate.data.sampling import generate_lhs_samples
|
||||
from femsurrogate.data.schema import TARGET_COLUMNS, BeamParameters
|
||||
|
||||
|
||||
def test_run_beam2d_case_returns_physical_responses():
|
||||
result = run_beam2d_case(
|
||||
BeamParameters(L_m=2.0, b_m=0.05, h_m=0.1, E_pa=200e9, P_n=1000.0)
|
||||
)
|
||||
|
||||
assert result.tip_uy_m < 0.0
|
||||
assert result.max_abs_bending_stress_pa > 0.0
|
||||
assert result.mass_kg > 0.0
|
||||
assert result.compliance_j > 0.0
|
||||
|
||||
|
||||
def test_build_dataset_preserves_samples_and_adds_schema_columns():
|
||||
samples = generate_lhs_samples(DEFAULT_PARAMETER_BOUNDS, n=4, seed=20260521)
|
||||
dataset = build_dataset(samples)
|
||||
|
||||
assert len(dataset) == len(samples)
|
||||
pd.testing.assert_frame_equal(dataset[list(samples.columns)], samples)
|
||||
for column in ["A_m2", "I_m4", *TARGET_COLUMNS]:
|
||||
assert column in dataset.columns
|
||||
@@ -0,0 +1,42 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from femsurrogate.fea.element import local_frame_stiffness, transformation_matrix
|
||||
|
||||
|
||||
def test_local_frame_stiffness_has_expected_shape_and_symmetry():
|
||||
stiffness = local_frame_stiffness(E=200.0, A=3.0, Izz=5.0, L=7.0)
|
||||
|
||||
assert stiffness.shape == (6, 6)
|
||||
np.testing.assert_allclose(stiffness, stiffness.T)
|
||||
|
||||
|
||||
def test_local_frame_stiffness_contains_clockwise_rotation_terms():
|
||||
E = 210.0
|
||||
A = 2.0
|
||||
Izz = 4.0
|
||||
L = 3.0
|
||||
|
||||
stiffness = local_frame_stiffness(E=E, A=A, Izz=Izz, L=L)
|
||||
|
||||
assert stiffness[0, 0] == pytest.approx(E * A / L)
|
||||
assert stiffness[0, 3] == pytest.approx(-E * A / L)
|
||||
assert stiffness[1, 1] == pytest.approx(12 * E * Izz / L**3)
|
||||
assert stiffness[1, 2] == pytest.approx(-6 * E * Izz / L**2)
|
||||
assert stiffness[2, 2] == pytest.approx(4 * E * Izz / L)
|
||||
assert stiffness[2, 5] == pytest.approx(2 * E * Izz / L)
|
||||
assert stiffness[4, 4] == pytest.approx(12 * E * Izz / L**3)
|
||||
assert stiffness[4, 5] == pytest.approx(6 * E * Izz / L**2)
|
||||
assert stiffness[5, 5] == pytest.approx(4 * E * Izz / L)
|
||||
|
||||
|
||||
def test_transformation_matrix_is_identity_for_horizontal_element():
|
||||
matrix = transformation_matrix(0.0, 0.0, 2.0, 0.0)
|
||||
|
||||
np.testing.assert_allclose(matrix, np.eye(6))
|
||||
|
||||
|
||||
def test_transformation_matrix_is_orthogonal_for_inclined_element():
|
||||
matrix = transformation_matrix(0.0, 0.0, 1.0, 1.0)
|
||||
|
||||
np.testing.assert_allclose(matrix.T @ matrix, np.eye(6), atol=1e-12)
|
||||
@@ -0,0 +1,41 @@
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
from matplotlib.figure import Figure
|
||||
|
||||
from femsurrogate.plotting.comparison import metrics_table, plot_metric_comparison
|
||||
from femsurrogate.plotting.diagnostics import plot_parity, plot_residuals
|
||||
from femsurrogate.surrogates.common import MetricsReport
|
||||
|
||||
|
||||
def _predictions() -> pd.DataFrame:
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"y_true": [-1.0, -2.0, -3.0],
|
||||
"y_pred": [-1.1, -1.9, -3.2],
|
||||
"residual": [0.1, -0.1, 0.2],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_diagnostic_plots_return_figures():
|
||||
parity = plot_parity(_predictions(), title="Parity")
|
||||
residuals = plot_residuals(_predictions(), title="Residuals")
|
||||
|
||||
assert isinstance(parity, Figure)
|
||||
assert isinstance(residuals, Figure)
|
||||
plt.close(parity)
|
||||
plt.close(residuals)
|
||||
|
||||
|
||||
def test_metrics_table_and_comparison_plot():
|
||||
reports = [
|
||||
MetricsReport("rsm", "tip_uy_m", 0.2, 0.1, 0.9, 0.01, 0.001),
|
||||
MetricsReport("gpr", "tip_uy_m", 0.1, 0.05, 0.95, 0.2, 0.01),
|
||||
]
|
||||
|
||||
table = metrics_table(reports)
|
||||
figure = plot_metric_comparison(table, metric="rmse", title="RMSE")
|
||||
|
||||
assert list(table["model_name"]) == ["gpr", "rsm"]
|
||||
assert isinstance(figure, Figure)
|
||||
plt.close(figure)
|
||||
@@ -0,0 +1,18 @@
|
||||
import importlib
|
||||
|
||||
import femsurrogate
|
||||
|
||||
|
||||
def test_package_exposes_version_string():
|
||||
assert isinstance(femsurrogate.__version__, str)
|
||||
assert femsurrogate.__version__
|
||||
|
||||
|
||||
def test_expected_subpackages_are_importable():
|
||||
for module_name in [
|
||||
"femsurrogate.fea",
|
||||
"femsurrogate.data",
|
||||
"femsurrogate.surrogates",
|
||||
"femsurrogate.plotting",
|
||||
]:
|
||||
assert importlib.import_module(module_name).__name__ == module_name
|
||||
@@ -0,0 +1,19 @@
|
||||
import pandas as pd
|
||||
|
||||
from femsurrogate.data.bounds import DEFAULT_PARAMETER_BOUNDS
|
||||
from femsurrogate.data.sampling import generate_lhs_samples
|
||||
|
||||
|
||||
def test_lhs_samples_are_reproducible_for_fixed_seed():
|
||||
first = generate_lhs_samples(DEFAULT_PARAMETER_BOUNDS, n=8, seed=20260521)
|
||||
second = generate_lhs_samples(DEFAULT_PARAMETER_BOUNDS, n=8, seed=20260521)
|
||||
|
||||
pd.testing.assert_frame_equal(first, second)
|
||||
|
||||
|
||||
def test_lhs_samples_have_expected_columns_and_bounds():
|
||||
samples = generate_lhs_samples(DEFAULT_PARAMETER_BOUNDS, n=16, seed=20260521)
|
||||
|
||||
assert list(samples.columns) == list(DEFAULT_PARAMETER_BOUNDS)
|
||||
for column, bounds in DEFAULT_PARAMETER_BOUNDS.items():
|
||||
assert samples[column].between(bounds.lower, bounds.upper).all()
|
||||
@@ -0,0 +1,67 @@
|
||||
import pandas as pd
|
||||
from sklearn.linear_model import LinearRegression
|
||||
|
||||
from femsurrogate.surrogates.common import evaluate_model, split_dataset
|
||||
|
||||
|
||||
def _toy_dataset() -> pd.DataFrame:
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"x1": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
|
||||
"x2": [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5],
|
||||
"target": [1.0, 2.5, 4.0, 5.5, 7.0, 8.5, 10.0, 11.5],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_split_dataset_is_reproducible():
|
||||
dataset = _toy_dataset()
|
||||
first = split_dataset(
|
||||
dataset,
|
||||
feature_columns=["x1", "x2"],
|
||||
target_column="target",
|
||||
test_size=0.25,
|
||||
random_state=20260521,
|
||||
)
|
||||
second = split_dataset(
|
||||
dataset,
|
||||
feature_columns=["x1", "x2"],
|
||||
target_column="target",
|
||||
test_size=0.25,
|
||||
random_state=20260521,
|
||||
)
|
||||
|
||||
pd.testing.assert_frame_equal(first.X_train, second.X_train)
|
||||
pd.testing.assert_frame_equal(first.X_test, second.X_test)
|
||||
pd.testing.assert_series_equal(first.y_train, second.y_train)
|
||||
pd.testing.assert_series_equal(first.y_test, second.y_test)
|
||||
|
||||
|
||||
def test_evaluate_model_returns_metrics_and_predictions():
|
||||
dataset = _toy_dataset()
|
||||
split = split_dataset(
|
||||
dataset,
|
||||
feature_columns=["x1", "x2"],
|
||||
target_column="target",
|
||||
test_size=0.25,
|
||||
random_state=20260521,
|
||||
)
|
||||
result = evaluate_model(
|
||||
LinearRegression(),
|
||||
split.X_train,
|
||||
split.X_test,
|
||||
split.y_train,
|
||||
split.y_test,
|
||||
model_name="linear",
|
||||
target_column="target",
|
||||
)
|
||||
|
||||
assert result.metrics.model_name == "linear"
|
||||
assert result.metrics.target_column == "target"
|
||||
assert result.metrics.rmse >= 0.0
|
||||
assert result.metrics.mae >= 0.0
|
||||
assert result.metrics.r2 <= 1.0
|
||||
assert result.metrics.fit_time_s >= 0.0
|
||||
assert result.metrics.predict_time_s >= 0.0
|
||||
assert list(result.predictions.columns) == ["y_true", "y_pred", "residual"]
|
||||
assert len(result.predictions) == len(split.y_test)
|
||||
@@ -0,0 +1,47 @@
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.exceptions import ConvergenceWarning
|
||||
|
||||
from femsurrogate.surrogates.registry import MODEL_NAMES, make_model
|
||||
|
||||
|
||||
def _toy_regression_data():
|
||||
X = pd.DataFrame(
|
||||
{
|
||||
"L_m": np.linspace(1.0, 3.0, 12),
|
||||
"b_m": np.linspace(0.02, 0.08, 12),
|
||||
"h_m": np.linspace(0.04, 0.16, 12),
|
||||
"E_pa": np.linspace(100e9, 220e9, 12),
|
||||
"P_n": np.linspace(100.0, 2000.0, 12),
|
||||
}
|
||||
)
|
||||
y = -X["P_n"] * X["L_m"] ** 3 / (3.0 * X["E_pa"] * X["b_m"] * X["h_m"] ** 3)
|
||||
return X, y
|
||||
|
||||
|
||||
def test_registry_exposes_expected_surrogate_names():
|
||||
assert MODEL_NAMES == ("rsm", "gpr", "random_forest", "gradient_boosting", "mlp")
|
||||
|
||||
|
||||
def test_make_model_builds_estimators_that_fit_and_predict():
|
||||
X, y = _toy_regression_data()
|
||||
fast_overrides = {
|
||||
"random_forest": {"n_estimators": 5, "n_jobs": 1},
|
||||
"gradient_boosting": {"n_estimators": 5},
|
||||
"mlp": {"hidden_layer_sizes": (4,), "max_iter": 25, "early_stopping": False},
|
||||
}
|
||||
|
||||
for model_name in MODEL_NAMES:
|
||||
model = make_model(
|
||||
model_name,
|
||||
random_state=20260521,
|
||||
**fast_overrides.get(model_name, {}),
|
||||
)
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", category=ConvergenceWarning)
|
||||
model.fit(X, y)
|
||||
predictions = model.predict(X.iloc[:3])
|
||||
|
||||
assert predictions.shape == (3,)
|
||||
Reference in New Issue
Block a user