initial commit FESurrogateModelTutorial

This commit is contained in:
김경종
2026-05-21 17:03:51 +09:00
parent 93665d9ee6
commit 43b86669fa
122 changed files with 7929 additions and 0 deletions
+1
View File
@@ -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,)