diff --git a/PLAN.md b/PLAN.md index 598df2e..044a335 100644 --- a/PLAN.md +++ b/PLAN.md @@ -13,7 +13,7 @@ Every new agent session must read this file together with `PROGRESS.md` before p - If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it. ## Current Objective -Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-09 material matrix, transform, and `2 x 2 x 2` integration scaffolding. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset. +Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-10 MITC4 stiffness, internal force, six-DOF transform, and drilling stabilization. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset. ## Required Reading For New Agents 1. `AGENTS.md` @@ -36,7 +36,7 @@ Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebase ## Active Phase Files - Active phase directory: `phases/1-linear-static-mitc4-rebaseline` - Execute with: `python scripts/execute.py 1-linear-static-mitc4-rebaseline` -- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step3.md` is complete and revalidated the Phase 1 parser/domain subset; `step4.md` is complete and strengthened validation/singular diagnostics; `step5.md` is complete and revalidated the DofManager/reaction foundation; `step6.md` is complete and revalidated the minimum result model plus displacement CSV comparator; `step7.md` is complete and revalidated MITC4 natural coordinates, tying points, center directors, and integration bases; `step8.md` is complete and revalidated degenerated-continuum displacement, direct covariant strain rows, and MITC shear tying rows; `step15.md` is the independent evaluator closeout. +- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step3.md` is complete and revalidated the Phase 1 parser/domain subset; `step4.md` is complete and strengthened validation/singular diagnostics; `step5.md` is complete and revalidated the DofManager/reaction foundation; `step6.md` is complete and revalidated the minimum result model plus displacement CSV comparator; `step7.md` is complete and revalidated MITC4 natural coordinates, tying points, center directors, and integration bases; `step8.md` is complete and revalidated degenerated-continuum displacement, direct covariant strain rows, and MITC shear tying rows; `step9.md` is complete and revalidated plane-stress material, convected-to-local transform, and `2 x 2 x 2` material integration scaffolding; `step15.md` is the independent evaluator closeout. - Every step file contains a sprint contract with objective, required reading, scope, allowed files, explicit non-goals, tests to write first, reference artifacts, acceptance command, evaluator checklist, and handoff requirements. - Historical phase directory: `phases/1-linear-static-mitc4` - Historical phase status: blocked/superseded. Do not resume the old P1-15/P1-16 path unless the user explicitly requests recovery of that exact phase. @@ -76,7 +76,7 @@ Each gate should be satisfied before moving to the next implementation band unle | G1 - Build and validation | satisfied | Build system, test framework, and `scripts/validate_workspace.py` run real checks. | Validation command output | | G2 - Parser and domain | satisfied | Parser subset revalidated in step 3; validation and singular diagnostics revalidated in step 4. | Parser acceptance/rejection tests, validation negative tests, and validation output | | G3 - DOF/math/results infrastructure | partial | Core aliases, DOF mapping, validation harness, model diagnostic context, DofManager, sparse-connectivity inputs, full-vector reaction formula, result model metadata, and displacement CSV comparator were revalidated in steps 2, 5, and 6; assembly remains for step 12. | P1R-02, P1R-05, and P1R-06 validation output | -| G4 - MITC4 element readiness | partial | MITC4 formulation was rewritten from local papers; Steps 7 and 8 rebuilt geometry/director/local-basis scaffolding, displacement interpolation, direct covariant strain rows, and MITC shear tying rows, while material/integration, stiffness/drilling, and patch benchmarks remain for steps 9 through 11. | P1R-07 and P1R-08 validation output; future element tests | +| G4 - MITC4 element readiness | partial | MITC4 formulation was rewritten from local papers; Steps 7 through 9 rebuilt geometry/director/local-basis scaffolding, displacement interpolation, direct covariant strain rows, MITC shear tying rows, plane-stress material, convected-to-local transform, and `2 x 2 x 2` material integration scaffolding, while stiffness/drilling and patch benchmarks remain for steps 10 and 11. | P1R-07 through P1R-09 validation output; future element tests | | G5 - End-to-end solver | reopened | Linear static path must be revalidated through steps 13 and 14 after the MITC4 rebuild and `quad_02` compatibility path. | Future integration/reference regression output | ## Phase 1 Implementation Milestones @@ -90,7 +90,7 @@ All milestones are intended to become one or more self-contained sprint contract | P1R-06 | completed | results generator | Rebuild minimum results model and displacement CSV comparator. | none | U/RF schema tests and CSV comparator tests | | P1R-07 | completed | MITC4 generator | Implement MITC4 geometry, node order, tying points, directors, and local bases. | none | Shape/basis/diagnostic tests | | P1R-08 | completed | MITC4 generator | Implement degenerated-continuum displacement, covariant strain rows, and MITC shear tying. | P1R-07 | Finite-difference and tying interpolation tests | -| P1R-09 | pending | MITC4 generator | Implement material matrix, transform, and `2 x 2 x 2` integration scaffolding. | P1R-08 | Material/integration tests | +| P1R-09 | completed | MITC4 generator | Implement material matrix, transform, and `2 x 2 x 2` integration scaffolding. | P1R-08 | Material/integration tests | | P1R-10 | pending | MITC4 generator | Assemble MITC4 stiffness/internal force with six-DOF transform and drilling stabilization. | P1R-09, P1R-05 | Symmetry, rigid body, drilling sensitivity tests | | P1R-11 | pending | verification generator | Add MITC4 patch, locking-sensitivity, and benchmark tests. | P1R-10 | Membrane/bending/shear/twist/locking tests | | P1R-12 | pending | assembly generator | Rebuild assembly, solver adapter boundary, constrained solve, and full-vector RF recovery. | P1R-05, P1R-10 | Assembly and full-vector reaction tests | diff --git a/PROGRESS.md b/PROGRESS.md index 9453556..2c60639 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -13,10 +13,36 @@ Every new agent session must read this file together with `PLAN.md` before plann - Do not remove history unless the user explicitly asks for archival cleanup. ## Current Status -Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 8 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, the Phase 1 parser/domain subset, validation/singular diagnostics, DofManager/reaction foundation, minimum result model metadata, displacement CSV comparator foundation, MITC4 geometry/director scaffolding, and MITC4 displacement/strain/tying row scaffolding have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset. +Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 9 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, the Phase 1 parser/domain subset, validation/singular diagnostics, DofManager/reaction foundation, minimum result model metadata, displacement CSV comparator foundation, MITC4 geometry/director scaffolding, MITC4 displacement/strain/tying row scaffolding, and MITC4 material/transform/integration scaffolding have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset. ## Completed Work +### 2026-05-04 - P1R-09 MITC4 material integration scaffolding completed +Author: Codex + +Changed files: +- `include/fesa/fesa.hpp` +- `tests/test_main.cpp` +- `phases/1-linear-static-mitc4-rebaseline/index.json` +- `PLAN.md` +- `PROGRESS.md` + +Summary: +- Added tests and implementation for the documented isotropic local plane-stress shell material matrix in strain order `[eps11, eps22, eps33, gamma23, gamma13, gamma12]`, with `sigma_33 = 0` and transverse shear correction `kappa = 5/6`. +- Added material diagnostics for invalid elastic modulus, Poisson ratio, and shear correction factor. +- Added `2 x 2 x 2` Gauss integration point infrastructure with unit weights and total natural-domain weight `8`. +- Added covariant-to-local strain transformation and `D_convected = T^T D_hat T`, including identity and flat-element energy-equivalence tests. +- Added MITC4 material integration sample scaffolding that carries integration basis, tied strain rows, local material, strain transform, and convected material for the later stiffness rebuild. + +Verification: +- First ran `python scripts/validate_workspace.py` after adding Step 9 tests; it failed as expected because the MITC4 material/integration APIs did not exist yet. +- After implementation, `python scripts/validate_workspace.py` configured CMake, built `fesa_core` and `fesa_tests`, and ran CTest successfully. +- CTest result: 1 test executable passed. + +Follow-up: +- Continue with P1R-10 MITC4 stiffness/internal force rebuild, six-DOF local-to-global transform, and drilling stabilization. +- The legacy `MITC4ElementKernel::stiffness` path still needs to be rebuilt against the Step 7 through Step 9 formulation helpers; Step 9 intentionally did not add reduced integration, hourglass logic, stress/result output recovery, or the final drilling stiffness policy. + ### 2026-05-04 - P1R-08 MITC4 covariant strain tying completed Author: Codex diff --git a/include/fesa/fesa.hpp b/include/fesa/fesa.hpp index e75d0d2..001af04 100644 --- a/include/fesa/fesa.hpp +++ b/include/fesa/fesa.hpp @@ -1304,6 +1304,7 @@ struct MITC4IntegrationBasis { using MITC4ElementDofVector = std::array; using MITC4StrainVector = std::array; using MITC4StrainRow = std::array; +using MITC4MaterialMatrix = std::array, 6>; enum class MITC4StrainComponent { Eps11 = 0, @@ -1359,6 +1360,54 @@ struct MITC4StrainRows { } }; +struct MITC4MaterialMatrixEvaluation { + MITC4MaterialMatrix matrix{}; + std::vector diagnostics; + + bool ok() const { + return !hasError(diagnostics); + } +}; + +struct MITC4StrainTransform { + MITC4MaterialMatrix matrix{}; + std::vector diagnostics; + + bool ok() const { + return !hasError(diagnostics); + } +}; + +struct MITC4IntegrationPoint { + Real xi = 0.0; + Real eta = 0.0; + Real zeta = 0.0; + Real weight = 0.0; +}; + +struct MITC4MaterialIntegrationSample { + MITC4IntegrationPoint point; + MITC4IntegrationBasis basis; + MITC4StrainRows strain_rows; + MITC4MaterialMatrix local_material{}; + MITC4MaterialMatrix strain_transform{}; + MITC4MaterialMatrix convected_material{}; + std::vector diagnostics; + + bool ok() const { + return !hasError(diagnostics); + } +}; + +struct MITC4MaterialIntegrationData { + std::vector samples; + std::vector diagnostics; + + bool ok() const { + return !hasError(diagnostics); + } +}; + inline Vec3 globalEX() { return {1.0, 0.0, 0.0}; } @@ -1624,6 +1673,209 @@ inline MITC4StrainRows mitc4TiedCovariantStrainRows(const MITC4Geometry& geometr return result; } +inline std::array mitc4GaussQuadrature2x2x2() { + const Real gauss = 1.0 / std::sqrt(3.0); + const std::array points = {-gauss, gauss}; + std::array integration_points{}; + std::size_t index = 0; + for (Real xi : points) { + for (Real eta : points) { + for (Real zeta : points) { + integration_points[index++] = {xi, eta, zeta, 1.0}; + } + } + } + return integration_points; +} + +inline MITC4MaterialMatrixEvaluation mitc4PlaneStressMaterialMatrix(Real elastic_modulus, Real poisson_ratio, + Real shear_correction = 5.0 / 6.0, + Real tolerance = 1.0e-12) { + MITC4MaterialMatrixEvaluation result; + if (!isFinite(elastic_modulus) || elastic_modulus <= tolerance) { + result.diagnostics.push_back( + mitc4Diagnostic("FESA-MITC4-MATERIAL", "MITC4 elastic modulus must be positive and finite")); + } + if (!isFinite(poisson_ratio) || poisson_ratio <= -1.0 || poisson_ratio >= 0.5) { + result.diagnostics.push_back( + mitc4Diagnostic("FESA-MITC4-POISSON", "MITC4 isotropic Poisson ratio must satisfy -1 < nu < 0.5")); + } + if (!isFinite(shear_correction) || shear_correction <= tolerance) { + result.diagnostics.push_back( + mitc4Diagnostic("FESA-MITC4-SHEAR-CORRECTION", "MITC4 shear correction factor must be positive and finite")); + } + if (hasError(result.diagnostics)) { + return result; + } + + const Real scale = elastic_modulus / (1.0 - poisson_ratio * poisson_ratio); + const Real shear_modulus = elastic_modulus / (2.0 * (1.0 + poisson_ratio)); + const std::size_t eps11 = strainComponentIndex(MITC4StrainComponent::Eps11); + const std::size_t eps22 = strainComponentIndex(MITC4StrainComponent::Eps22); + const std::size_t gamma23 = strainComponentIndex(MITC4StrainComponent::Gamma23); + const std::size_t gamma13 = strainComponentIndex(MITC4StrainComponent::Gamma13); + const std::size_t gamma12 = strainComponentIndex(MITC4StrainComponent::Gamma12); + + result.matrix[eps11][eps11] = scale; + result.matrix[eps11][eps22] = poisson_ratio * scale; + result.matrix[eps22][eps11] = poisson_ratio * scale; + result.matrix[eps22][eps22] = scale; + result.matrix[gamma23][gamma23] = shear_correction * shear_modulus; + result.matrix[gamma13][gamma13] = shear_correction * shear_modulus; + result.matrix[gamma12][gamma12] = shear_modulus; + return result; +} + +inline MITC4StrainVector multiplyMITC4MaterialMatrix(const MITC4MaterialMatrix& matrix, const MITC4StrainVector& vector) { + MITC4StrainVector result{}; + for (std::size_t row = 0; row < 6; ++row) { + for (std::size_t col = 0; col < 6; ++col) { + result[row] += matrix[row][col] * vector[col]; + } + } + return result; +} + +inline Real dotMITC4Vector(const MITC4StrainVector& a, const MITC4StrainVector& b) { + Real result = 0.0; + for (std::size_t i = 0; i < 6; ++i) { + result += a[i] * b[i]; + } + return result; +} + +inline MITC4MaterialMatrix mitc4TransformMaterialMatrix(const MITC4MaterialMatrix& local_material, + const MITC4MaterialMatrix& covariant_to_local) { + MITC4MaterialMatrix result{}; + for (std::size_t i = 0; i < 6; ++i) { + for (std::size_t j = 0; j < 6; ++j) { + Real value = 0.0; + for (std::size_t a = 0; a < 6; ++a) { + for (std::size_t b = 0; b < 6; ++b) { + value += covariant_to_local[a][i] * local_material[a][b] * covariant_to_local[b][j]; + } + } + result[i][j] = value; + } + } + return result; +} + +inline std::array, 3> mitc4TensorFromEngineeringComponent(std::size_t component) { + std::array, 3> tensor{}; + switch (static_cast(component)) { + case MITC4StrainComponent::Eps11: + tensor[0][0] = 1.0; + break; + case MITC4StrainComponent::Eps22: + tensor[1][1] = 1.0; + break; + case MITC4StrainComponent::Eps33: + tensor[2][2] = 1.0; + break; + case MITC4StrainComponent::Gamma23: + tensor[1][2] = 0.5; + tensor[2][1] = 0.5; + break; + case MITC4StrainComponent::Gamma13: + tensor[0][2] = 0.5; + tensor[2][0] = 0.5; + break; + case MITC4StrainComponent::Gamma12: + tensor[0][1] = 0.5; + tensor[1][0] = 0.5; + break; + } + return tensor; +} + +inline MITC4StrainVector mitc4EngineeringVectorFromTensor(const std::array, 3>& tensor) { + MITC4StrainVector vector{}; + vector[strainComponentIndex(MITC4StrainComponent::Eps11)] = tensor[0][0]; + vector[strainComponentIndex(MITC4StrainComponent::Eps22)] = tensor[1][1]; + vector[strainComponentIndex(MITC4StrainComponent::Eps33)] = tensor[2][2]; + vector[strainComponentIndex(MITC4StrainComponent::Gamma23)] = 2.0 * tensor[1][2]; + vector[strainComponentIndex(MITC4StrainComponent::Gamma13)] = 2.0 * tensor[0][2]; + vector[strainComponentIndex(MITC4StrainComponent::Gamma12)] = 2.0 * tensor[0][1]; + return vector; +} + +inline MITC4StrainTransform mitc4CovariantToLocalStrainTransform(const MITC4IntegrationBasis& basis, + Real tolerance = 1.0e-12) { + MITC4StrainTransform result; + result.diagnostics = basis.diagnostics; + const Real jacobian = dot(cross(basis.g1, basis.g2), basis.g3); + if (!isFinite(jacobian) || std::fabs(jacobian) <= tolerance) { + result.diagnostics.push_back( + mitc4Diagnostic("FESA-MITC4-SINGULAR-JACOBIAN", "MITC4 material transform Jacobian is near zero")); + return result; + } + if (hasError(result.diagnostics)) { + return result; + } + + const std::array contravariant = { + (1.0 / jacobian) * cross(basis.g2, basis.g3), + (1.0 / jacobian) * cross(basis.g3, basis.g1), + (1.0 / jacobian) * cross(basis.g1, basis.g2)}; + const std::array local = {basis.local.e1, basis.local.e2, basis.local.e3}; + std::array, 3> direction_cosines{}; + for (std::size_t local_axis = 0; local_axis < 3; ++local_axis) { + for (std::size_t convected_axis = 0; convected_axis < 3; ++convected_axis) { + direction_cosines[local_axis][convected_axis] = dot(local[local_axis], contravariant[convected_axis]); + } + } + + for (std::size_t column = 0; column < 6; ++column) { + const auto covariant_tensor = mitc4TensorFromEngineeringComponent(column); + std::array, 3> local_tensor{}; + for (std::size_t a = 0; a < 3; ++a) { + for (std::size_t b = 0; b < 3; ++b) { + for (std::size_t i = 0; i < 3; ++i) { + for (std::size_t j = 0; j < 3; ++j) { + local_tensor[a][b] += direction_cosines[a][i] * direction_cosines[b][j] * covariant_tensor[i][j]; + } + } + } + } + const auto local_vector = mitc4EngineeringVectorFromTensor(local_tensor); + for (std::size_t row = 0; row < 6; ++row) { + result.matrix[row][column] = local_vector[row]; + } + } + return result; +} + +inline MITC4MaterialIntegrationData mitc4BuildMaterialIntegrationData(const MITC4Geometry& geometry, Real elastic_modulus, + Real poisson_ratio, + Real shear_correction = 5.0 / 6.0) { + MITC4MaterialIntegrationData data; + const auto material = mitc4PlaneStressMaterialMatrix(elastic_modulus, poisson_ratio, shear_correction); + appendDiagnostics(data.diagnostics, material.diagnostics); + if (hasError(data.diagnostics)) { + return data; + } + + for (const MITC4IntegrationPoint& point : mitc4GaussQuadrature2x2x2()) { + MITC4MaterialIntegrationSample sample; + sample.point = point; + sample.local_material = material.matrix; + sample.basis = computeMITC4IntegrationBasis(geometry, point.xi, point.eta, point.zeta); + appendDiagnostics(sample.diagnostics, sample.basis.diagnostics); + const auto transform = mitc4CovariantToLocalStrainTransform(sample.basis); + sample.strain_transform = transform.matrix; + appendDiagnostics(sample.diagnostics, transform.diagnostics); + sample.strain_rows = mitc4TiedCovariantStrainRows(geometry, point.xi, point.eta, point.zeta); + appendDiagnostics(sample.diagnostics, sample.strain_rows.diagnostics); + if (!hasError(sample.diagnostics)) { + sample.convected_material = mitc4TransformMaterialMatrix(sample.local_material, sample.strain_transform); + } + appendDiagnostics(data.diagnostics, sample.diagnostics); + data.samples.push_back(sample); + } + return data; +} + inline LocalBasis computeLocalBasis(const std::array& coordinates) { const MITC4Geometry geometry = buildMITC4Geometry(coordinates, 1.0); if (!geometry.ok()) { diff --git a/phases/1-linear-static-mitc4-rebaseline/index.json b/phases/1-linear-static-mitc4-rebaseline/index.json index 755b2bc..a4c2154 100644 --- a/phases/1-linear-static-mitc4-rebaseline/index.json +++ b/phases/1-linear-static-mitc4-rebaseline/index.json @@ -11,7 +11,7 @@ { "step": 6, "name": "results-comparator-foundation", "status": "completed" }, { "step": 7, "name": "mitc4-geometry-directors", "status": "completed" }, { "step": 8, "name": "mitc4-covariant-strain-tying", "status": "completed" }, - { "step": 9, "name": "mitc4-material-integration", "status": "pending" }, + { "step": 9, "name": "mitc4-material-integration", "status": "completed" }, { "step": 10, "name": "mitc4-stiffness-drilling", "status": "pending" }, { "step": 11, "name": "mitc4-patch-benchmark-tests", "status": "pending" }, { "step": 12, "name": "assembly-sparse-solver-path", "status": "pending" }, diff --git a/tests/test_main.cpp b/tests/test_main.cpp index d04136e..3ab4c92 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -1070,6 +1070,155 @@ FESA_TEST(mitc4_mitc_gauss_shear_rows_are_interpolated_from_tying_rows) { } } +FESA_TEST(mitc4_plane_stress_material_matrix_uses_documented_order_and_shear_correction) { + constexpr fesa::Real elastic_modulus = 210.0; + constexpr fesa::Real poisson_ratio = 0.3; + constexpr fesa::Real kappa = 5.0 / 6.0; + const auto law = fesa::mitc4PlaneStressMaterialMatrix(elastic_modulus, poisson_ratio, kappa); + FESA_CHECK(law.ok()); + const auto eps11 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Eps11); + const auto eps22 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Eps22); + const auto eps33 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Eps33); + const auto gamma23 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma23); + const auto gamma13 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma13); + const auto gamma12 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma12); + const fesa::Real plane_stress_scale = elastic_modulus / (1.0 - poisson_ratio * poisson_ratio); + const fesa::Real shear_modulus = elastic_modulus / (2.0 * (1.0 + poisson_ratio)); + + FESA_CHECK(fesa::mitc4StrainComponentLabels() == + (std::array{"eps11", "eps22", "eps33", "gamma23", "gamma13", "gamma12"})); + FESA_CHECK_NEAR(law.matrix[eps11][eps11], plane_stress_scale, 1.0e-12); + FESA_CHECK_NEAR(law.matrix[eps11][eps22], poisson_ratio * plane_stress_scale, 1.0e-12); + FESA_CHECK_NEAR(law.matrix[eps22][eps11], poisson_ratio * plane_stress_scale, 1.0e-12); + FESA_CHECK_NEAR(law.matrix[eps22][eps22], plane_stress_scale, 1.0e-12); + for (std::size_t component = 0; component < 6; ++component) { + FESA_CHECK_NEAR(law.matrix[eps33][component], 0.0, 1.0e-15); + FESA_CHECK_NEAR(law.matrix[component][eps33], 0.0, 1.0e-15); + } + FESA_CHECK_NEAR(law.matrix[gamma23][gamma23], kappa * shear_modulus, 1.0e-12); + FESA_CHECK_NEAR(law.matrix[gamma13][gamma13], kappa * shear_modulus, 1.0e-12); + FESA_CHECK_NEAR(law.matrix[gamma12][gamma12], shear_modulus, 1.0e-12); + FESA_CHECK_NEAR(law.matrix[gamma13][gamma23], 0.0, 1.0e-15); + + fesa::MITC4StrainVector strain{}; + strain[gamma23] = 2.0; + strain[gamma13] = 3.0; + strain[gamma12] = 4.0; + const auto stress = fesa::multiplyMITC4MaterialMatrix(law.matrix, strain); + FESA_CHECK_NEAR(stress[gamma23], 2.0 * kappa * shear_modulus, 1.0e-12); + FESA_CHECK_NEAR(stress[gamma13], 3.0 * kappa * shear_modulus, 1.0e-12); + FESA_CHECK_NEAR(stress[gamma12], 4.0 * shear_modulus, 1.0e-12); +} + +FESA_TEST(mitc4_material_matrix_reports_invalid_elastic_inputs) { + auto invalid_e = fesa::mitc4PlaneStressMaterialMatrix(-1.0, 0.3); + FESA_CHECK(!invalid_e.ok()); + FESA_CHECK(fesa::containsDiagnostic(invalid_e.diagnostics, "FESA-MITC4-MATERIAL")); + + auto invalid_nu = fesa::mitc4PlaneStressMaterialMatrix(1000.0, 0.5); + FESA_CHECK(!invalid_nu.ok()); + FESA_CHECK(fesa::containsDiagnostic(invalid_nu.diagnostics, "FESA-MITC4-POISSON")); + + auto invalid_kappa = fesa::mitc4PlaneStressMaterialMatrix(1000.0, 0.25, 0.0); + FESA_CHECK(!invalid_kappa.ok()); + FESA_CHECK(fesa::containsDiagnostic(invalid_kappa.diagnostics, "FESA-MITC4-SHEAR-CORRECTION")); +} + +FESA_TEST(mitc4_gauss_integration_uses_2x2x2_points_and_unit_weights) { + const auto points = fesa::mitc4GaussQuadrature2x2x2(); + FESA_CHECK(points.size() == 8); + const fesa::Real gauss = 1.0 / std::sqrt(3.0); + fesa::Real total_weight = 0.0; + int positive_xi = 0; + int positive_eta = 0; + int positive_zeta = 0; + for (const auto& point : points) { + FESA_CHECK_NEAR(std::fabs(point.xi), gauss, 1.0e-15); + FESA_CHECK_NEAR(std::fabs(point.eta), gauss, 1.0e-15); + FESA_CHECK_NEAR(std::fabs(point.zeta), gauss, 1.0e-15); + FESA_CHECK_NEAR(point.weight, 1.0, 1.0e-15); + total_weight += point.weight; + positive_xi += point.xi > 0.0 ? 1 : 0; + positive_eta += point.eta > 0.0 ? 1 : 0; + positive_zeta += point.zeta > 0.0 ? 1 : 0; + } + FESA_CHECK_NEAR(total_weight, 8.0, 1.0e-15); + FESA_CHECK(positive_xi == 4); + FESA_CHECK(positive_eta == 4); + FESA_CHECK(positive_zeta == 4); +} + +FESA_TEST(mitc4_covariant_to_local_transform_is_identity_for_orthonormal_basis) { + fesa::MITC4IntegrationBasis basis; + basis.g1 = {1.0, 0.0, 0.0}; + basis.g2 = {0.0, 1.0, 0.0}; + basis.g3 = {0.0, 0.0, 1.0}; + basis.local = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}; + basis.jacobian = 1.0; + const auto transform = fesa::mitc4CovariantToLocalStrainTransform(basis); + FESA_CHECK(transform.ok()); + for (std::size_t row = 0; row < 6; ++row) { + for (std::size_t col = 0; col < 6; ++col) { + FESA_CHECK_NEAR(transform.matrix[row][col], row == col ? 1.0 : 0.0, 1.0e-15); + } + } +} + +FESA_TEST(mitc4_flat_element_material_transform_scales_convected_strains_to_local_frame) { + const std::array coords = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}}; + const auto geometry = fesa::buildMITC4Geometry(coords, 0.2); + FESA_CHECK(geometry.ok()); + const auto basis = fesa::computeMITC4IntegrationBasis(geometry, 0.0, 0.0, 0.0); + FESA_CHECK(basis.ok()); + const auto transform = fesa::mitc4CovariantToLocalStrainTransform(basis); + FESA_CHECK(transform.ok()); + + const std::array expected_diagonal = {4.0, 4.0, 100.0, 20.0, 20.0, 4.0}; + for (std::size_t row = 0; row < 6; ++row) { + for (std::size_t col = 0; col < 6; ++col) { + FESA_CHECK_NEAR(transform.matrix[row][col], row == col ? expected_diagonal[row] : 0.0, 1.0e-13); + } + } + + const auto law = fesa::mitc4PlaneStressMaterialMatrix(1000.0, 0.25); + FESA_CHECK(law.ok()); + const auto convected = fesa::mitc4TransformMaterialMatrix(law.matrix, transform.matrix); + fesa::MITC4StrainVector local_strain{}; + local_strain[fesa::strainComponentIndex(fesa::MITC4StrainComponent::Eps11)] = 0.02; + local_strain[fesa::strainComponentIndex(fesa::MITC4StrainComponent::Eps22)] = -0.01; + local_strain[fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma12)] = 0.03; + + fesa::MITC4StrainVector convected_strain{}; + for (std::size_t component = 0; component < 6; ++component) { + convected_strain[component] = local_strain[component] / expected_diagonal[component]; + } + const auto local_stress = fesa::multiplyMITC4MaterialMatrix(law.matrix, local_strain); + const auto convected_stress = fesa::multiplyMITC4MaterialMatrix(convected, convected_strain); + const fesa::Real local_energy = fesa::dotMITC4Vector(local_strain, local_stress); + const fesa::Real convected_energy = fesa::dotMITC4Vector(convected_strain, convected_stress); + FESA_CHECK_NEAR(convected_energy, local_energy, 1.0e-12); +} + +FESA_TEST(mitc4_material_integration_samples_carry_tied_rows_basis_and_transformed_material) { + const std::array coords = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}}; + const auto geometry = fesa::buildMITC4Geometry(coords, 0.2); + FESA_CHECK(geometry.ok()); + const auto data = fesa::mitc4BuildMaterialIntegrationData(geometry, 1000.0, 0.3); + FESA_CHECK(data.ok()); + FESA_CHECK(data.samples.size() == 8); + for (const auto& sample : data.samples) { + FESA_CHECK(sample.ok()); + FESA_CHECK_NEAR(sample.point.weight, 1.0, 1.0e-15); + FESA_CHECK(sample.basis.ok()); + FESA_CHECK(sample.strain_rows.ok()); + for (std::size_t i = 0; i < 6; ++i) { + for (std::size_t j = 0; j < 6; ++j) { + FESA_CHECK_NEAR(sample.convected_material[i][j], sample.convected_material[j][i], 1.0e-10); + } + } + } +} + FESA_TEST(mitc4_shape_functions_and_stiffness_baseline) { auto shape = fesa::shapeFunctions(0.25, -0.5); const fesa::Real sum = shape.n[0] + shape.n[1] + shape.n[2] + shape.n[3];