From 1fc3128ed53ea3530967e9d397fdd62c4cfda0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B2=BD=EC=A2=85?= Date: Thu, 11 Jun 2026 14:29:20 +0900 Subject: [PATCH] feat: implement 3d euler beam fortran kernel --- PLAN.md | 6 +- PROGRESS.md | 31 +- WORKNOTE.md | 1 + .../uel-3d-euler-beam-green.md | 66 +++++ phases/uel-3d-euler-beam/index.json | 3 +- scripts/fortran_toolchain.py | 15 +- scripts/test_fortran_toolchain.py | 10 +- scripts/test_validate_fortran.py | 43 ++- scripts/validate_fortran.py | 1 + src/fortran/uel_3d_euler_beam_abi_adapter.f90 | 121 ++++++++ src/fortran/uel_3d_euler_beam_kernel.f90 | 274 ++++++++++++++++++ tests/fortran/manifest.json | 17 ++ .../test_uel_3d_euler_beam_abi_adapter.f90 | 33 +++ .../fortran/test_uel_3d_euler_beam_kernel.f90 | 19 ++ 14 files changed, 614 insertions(+), 26 deletions(-) create mode 100644 docs/build-test-reports/uel-3d-euler-beam-green.md create mode 100644 src/fortran/uel_3d_euler_beam_abi_adapter.f90 create mode 100644 src/fortran/uel_3d_euler_beam_kernel.f90 create mode 100644 tests/fortran/test_uel_3d_euler_beam_abi_adapter.f90 create mode 100644 tests/fortran/test_uel_3d_euler_beam_kernel.f90 diff --git a/PLAN.md b/PLAN.md index cb21261..9d970bc 100644 --- a/PLAN.md +++ b/PLAN.md @@ -12,7 +12,7 @@ - Phase directory: `phases/uel-3d-euler-beam` - Current planned entry point: `python scripts/execute.py uel-3d-euler-beam` -- First pending step: `step7` Fortran implementation +- First pending step: `step8` validation readiness ## Planned Steps @@ -23,8 +23,8 @@ 5. Interface contract: `docs/io-definitions/uel-3d-euler-beam.md` 6. Test/reference model plan: `docs/reference-models/uel-3d-euler-beam.md` 7. RED no-Abaqus Fortran tests and manifest - completed -8. GREEN Fortran implementation - next -9. Validation, physics sanity, and readiness audit +8. GREEN Fortran implementation - completed +9. Validation, physics sanity, and readiness audit - next ## Success Criteria diff --git a/PROGRESS.md b/PROGRESS.md index 3ca775a..6a443db 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -5,8 +5,8 @@ - Active objective: 3D Euler-Bernoulli beam Abaqus/Standard `UEL` - Active phase: `phases/uel-3d-euler-beam` - Active owner: unassigned -- Current status: completed `phases/uel-3d-euler-beam/step6.md` RED no-Abaqus test creation step -- Next action: execute `phases/uel-3d-euler-beam/step7.md` Fortran implementation step +- Current status: completed `phases/uel-3d-euler-beam/step7.md` Fortran implementation step +- Next action: execute `phases/uel-3d-euler-beam/step8.md` validation-readiness step ## Completed @@ -40,6 +40,15 @@ - Created `tests/fortran/uel_3d_euler_beam/test_invalid_inputs.f90`. - Created `docs/build-test-reports/uel-3d-euler-beam-red.md`. - Updated `phases/uel-3d-euler-beam/index.json` step 6 to `completed`. +- Completed step 7 Fortran implementation for `uel-3d-euler-beam`. + - Created `src/fortran/uel_3d_euler_beam_kernel.f90`. + - Created `src/fortran/uel_3d_euler_beam_abi_adapter.f90`. + - Added source-smoke no-Abaqus tests under `tests/fortran/`. + - Updated `tests/fortran/manifest.json` with the source-smoke tests. + - Fixed `scripts/validate_fortran.py` to create per-test build directories. + - Fixed `scripts/fortran_toolchain.py` to use `ComSpec`/absolute `cmd.exe` and a minimal Windows PATH for oneAPI env setup. + - Created `docs/build-test-reports/uel-3d-euler-beam-green.md`. + - Updated `phases/uel-3d-euler-beam/index.json` step 7 to `completed`. ## In Progress @@ -51,28 +60,24 @@ ## Last Verification -Latest verification after completing step 6 RED no-Abaqus test creation: +Latest verification after completing step 7 Fortran implementation: ```bash -python -m json.tool tests/fortran/manifest.json python -m unittest discover -s scripts -p "test_*.py" -python scripts/validate_reference_artifacts.py python scripts/validate_fortran.py +python scripts/validate_reference_artifacts.py python scripts/validate_workspace.py -$env:HARNESS_FORTRAN_VALIDATION='detect'; python scripts/validate_workspace.py ``` -Result: RED evidence recorded. -- `python -m json.tool tests/fortran/manifest.json`: passed. -- `python -m unittest discover -s scripts -p "test_*.py"`: 56 tests passed. +Result: all passed. +- `python -m unittest discover -s scripts -p "test_*.py"`: 57 tests passed. +- `python scripts/validate_fortran.py`: six Fortran manifest executables compiled and passed with Intel `ifx` through oneAPI env script. - `python scripts/validate_reference_artifacts.py`: reference artifact metadata validation succeeded. -- `python scripts/validate_fortran.py`: default process failed before RED because child `cmd.exe` saw empty `%PATH%`; with minimal `PATH` normalization it reached `ifx` and failed as expected on missing `src/fortran/uel_3d_euler_beam_kernel.f90`. -- `python scripts/validate_workspace.py`: default process failed at the same environment issue; with minimal `PATH` normalization it failed at the expected missing production kernel source. -- `$env:HARNESS_FORTRAN_VALIDATION='detect'; python scripts/validate_workspace.py`: passed. +- `python scripts/validate_workspace.py`: succeeded. ## Next Agent Checklist - Read `AGENTS.md`, `PLAN.md`, `PROGRESS.md`, and `WORKNOTE.md`. - Confirm no other owner is active in this file. -- Start `phases/uel-3d-euler-beam/step7.md`. +- Start `phases/uel-3d-euler-beam/step8.md`. - Update this file when step status changes or before handing off. diff --git a/WORKNOTE.md b/WORKNOTE.md index 64904de..439a7ba 100644 --- a/WORKNOTE.md +++ b/WORKNOTE.md @@ -12,3 +12,4 @@ - `scripts/execute.py`는 phase step을 Codex subprocess로 실행하고, 각 step이 `phases//index.json`의 status를 직접 갱신해야 한다. Step 지시문에는 이 요구가 명확해야 한다. - `python scripts/validate_workspace.py`는 Fortran manifest가 없으면 `validate_fortran.py`에서 "No Fortran validation commands configured."를 출력하고 성공할 수 있다. 이것은 구현 전 scaffold 상태에서는 정상이다. - Step 6에서 `tests/fortran/manifest.json`을 추가한 뒤 기본 `python scripts/validate_fortran.py`가 nested `cmd` lookup에서 먼저 실패했다. PowerShell/Python은 `cmd.exe`를 찾지만 child `cmd.exe`가 `%PATH%`를 빈 값으로 보았다. RED compile evidence는 `$env:PATH='C:\Windows\System32;C:\Windows;C:\Users\user\miniforge3'; C:\Users\user\miniforge3\python.exe scripts/validate_fortran.py`로 확인했으며, 이때 `ifx`가 실행되고 missing `src/fortran/uel_3d_euler_beam_kernel.f90` compile failure가 발생했다. +- Step 7에서 위 PATH 문제는 `scripts/fortran_toolchain.py`가 `ComSpec`/절대 `cmd.exe`와 최소 Windows PATH를 사용하도록 수정해 해결했다. 또한 `scripts/validate_fortran.py`가 per-test build directory를 만들지 않아 `LNK1104`가 발생했으므로 `build_dir.mkdir(parents=True, exist_ok=True)`를 추가했다. diff --git a/docs/build-test-reports/uel-3d-euler-beam-green.md b/docs/build-test-reports/uel-3d-euler-beam-green.md new file mode 100644 index 0000000..f510c18 --- /dev/null +++ b/docs/build-test-reports/uel-3d-euler-beam-green.md @@ -0,0 +1,66 @@ +# 3D Euler-Bernoulli Beam UEL GREEN Test Report + +## Metadata +- feature_id: uel-3d-euler-beam +- source_red_report: `docs/build-test-reports/uel-3d-euler-beam-red.md` +- source_reference_models: `docs/reference-models/uel-3d-euler-beam.md` +- status: pass-for-workspace-validation +- owner_agent: implementation-agent +- date: 2026-06-11 + +## Implementation Scope + +Production source added in this step: + +- `src/fortran/uel_3d_euler_beam_kernel.f90` +- `src/fortran/uel_3d_euler_beam_abi_adapter.f90` + +The implementation covers the approved no-Abaqus scope: + +- 12-by-12 Euler-Bernoulli local stiffness matrix. +- Local frame construction from node coordinates and `PROPS(7:9)`. +- `K_global = T^T*k_local*T`. +- Static residual `RHS(1:12,1) = -K_global*U(1:12)`. +- `AMATRX` and `RHS` selection for `LFLAGS(3)=1`, `2`, and `5`. +- Shape, property, geometry, orientation, and unsupported-request diagnostics. +- Deterministic zeroing of `RHS`, `AMATRX`, and `ENERGY`; `PNEWDT` unchanged for valid calls. + +The fixed-form Abaqus `UEL` wrapper remains deferred because the approved step 7 no-Abaqus tests target the kernel and ABI adapter only. + +## Harness Support Changes + +Two validation harness fixes were required after the step 6 RED manifest became executable: + +- `scripts/validate_fortran.py` now creates each `build/fortran-tests//` directory before invoking Intel Fortran. +- `scripts/fortran_toolchain.py` now invokes the oneAPI environment script through `ComSpec` or discovered `cmd.exe` and sets a minimal Windows PATH for the child command shell. + +These changes are covered by updated Python harness tests in: + +- `scripts/test_validate_fortran.py` +- `scripts/test_fortran_toolchain.py` + +## Command Log Summary + +| order | command | exit_code | result | evidence | +| --- | --- | --- | --- | --- | +| 1 | `python scripts/test_validate_fortran.py -k build_commands_create` | 1 then 0 | RED then GREEN | failed before build-dir creation fix; passed after `build_dir.mkdir(...)` | +| 2 | `python scripts/test_fortran_toolchain.py -k wrap_command` | 1 then 0 | RED then GREEN | failed before absolute `cmd.exe` and minimal PATH wrapper; passed after wrapper update | +| 3 | `python -m unittest discover -s scripts -p "test_*.py"` | 0 | pass | 57 Python harness tests passed | +| 4 | `python scripts/validate_reference_artifacts.py` | 0 | pass | reference artifact metadata validation succeeded | +| 5 | `python scripts/validate_fortran.py` | 0 | pass | all six no-Abaqus Fortran manifest executables compiled and passed with Intel `ifx` through oneAPI env script | +| 6 | `python scripts/validate_workspace.py` | 0 | pass | reference validation and Fortran validation both succeeded | + +## No-Abaqus Fortran Tests Passed + +| manifest_test | result | +| --- | --- | +| `uel_3d_euler_beam_kernel_stiffness` | pass | +| `uel_3d_euler_beam_kernel_transform_modes` | pass | +| `uel_3d_euler_beam_abi_static` | pass | +| `uel_3d_euler_beam_invalid_inputs` | pass | +| `uel_3d_euler_beam_kernel_source_smoke` | pass | +| `uel_3d_euler_beam_abi_adapter_source_smoke` | pass | + +## Handoff to Step 8 + +Step 8 can proceed to validation readiness using the passing no-Abaqus evidence above. External Abaqus reference artifacts remain absent and must still be user-generated before solver-result comparison. diff --git a/phases/uel-3d-euler-beam/index.json b/phases/uel-3d-euler-beam/index.json index 51f3c3b..a62a684 100644 --- a/phases/uel-3d-euler-beam/index.json +++ b/phases/uel-3d-euler-beam/index.json @@ -47,7 +47,8 @@ { "step": 7, "name": "fortran-implementation", - "status": "pending" + "status": "completed", + "summary": "Implemented the approved no-Abaqus Fortran kernel and ABI adapter source layout, added narrow source smoke manifest tests, fixed Fortran validation harness build-dir and cmd PATH handling, and recorded GREEN evidence in docs/build-test-reports/uel-3d-euler-beam-green.md. Python harness, Fortran manifest, reference artifact validation, and workspace validation all passed." }, { "step": 8, diff --git a/scripts/fortran_toolchain.py b/scripts/fortran_toolchain.py index 6e7a1de..97f862e 100644 --- a/scripts/fortran_toolchain.py +++ b/scripts/fortran_toolchain.py @@ -72,10 +72,23 @@ def quote_path(path: str | Path) -> str: return shell_join([path]) +def cmd_executable(env: dict[str, str] | None = None) -> str: + env = env or os.environ + return env.get("ComSpec") or shutil.which("cmd.exe") or shutil.which("cmd") or "cmd" + + +def cmd_minimal_path(env: dict[str, str] | None = None) -> str: + env = env or os.environ + system_root = env.get("SystemRoot") or r"C:\Windows" + return f"{system_root}\\System32;{system_root}" + + def wrap_command(toolchain: FortranToolchain, args: list[str | Path]) -> str: command = shell_join(args) if toolchain.env_script is None: return command env_script = quote_path(toolchain.env_script) - return f'cmd /d /s /c "call {env_script} intel64 >nul && {command}"' + cmd = quote_path(cmd_executable()) + path = cmd_minimal_path() + return f'{cmd} /d /s /c "set "PATH={path}" && call {env_script} intel64 >nul && {command}"' diff --git a/scripts/test_fortran_toolchain.py b/scripts/test_fortran_toolchain.py index d3a74ca..ddcf7d9 100644 --- a/scripts/test_fortran_toolchain.py +++ b/scripts/test_fortran_toolchain.py @@ -65,9 +65,15 @@ class FortranToolchainTests(unittest.TestCase): env_script=Path(r"C:\Program Files (x86)\Intel\oneAPI\setvars.bat"), ) - command = fortran_toolchain.wrap_command(toolchain, ["ifx", "/nologo", "test.f90"]) + with patch.dict( + os.environ, + {"ComSpec": r"C:\Windows\System32\cmd.exe", "SystemRoot": r"C:\Windows"}, + clear=True, + ): + command = fortran_toolchain.wrap_command(toolchain, ["ifx", "/nologo", "test.f90"]) - self.assertIn("cmd /d /s /c", command) + self.assertIn(r"C:\Windows\System32\cmd.exe /d /s /c", command) + self.assertIn('set "PATH=C:\\Windows\\System32;C:\\Windows"', command) self.assertIn('call "C:\\Program Files (x86)\\Intel\\oneAPI\\setvars.bat" intel64', command) self.assertIn("ifx /nologo test.f90", command) diff --git a/scripts/test_validate_fortran.py b/scripts/test_validate_fortran.py index 81dc293..27e1cd7 100644 --- a/scripts/test_validate_fortran.py +++ b/scripts/test_validate_fortran.py @@ -3,6 +3,7 @@ import json import os import tempfile import unittest +from unittest import mock from pathlib import Path @@ -19,7 +20,7 @@ class ValidateFortranTests(unittest.TestCase): validate_fortran = load_validate_fortran() with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) - with unittest.mock.patch.dict(os.environ, {}, clear=True): + with mock.patch.dict(os.environ, {}, clear=True): commands = validate_fortran.discover_commands(root) self.assertEqual(commands, []) @@ -31,7 +32,7 @@ class ValidateFortranTests(unittest.TestCase): manifest = root / "tests" / "fortran" / "manifest.json" manifest.parent.mkdir(parents=True) manifest.write_text(json.dumps({"tests": []}), encoding="utf-8") - with unittest.mock.patch.dict(os.environ, {"HARNESS_FORTRAN_VALIDATION": "off"}, clear=True): + with mock.patch.dict(os.environ, {"HARNESS_FORTRAN_VALIDATION": "off"}, clear=True): commands = validate_fortran.discover_commands(root) self.assertEqual(commands, []) @@ -43,8 +44,8 @@ class ValidateFortranTests(unittest.TestCase): manifest = root / "tests" / "fortran" / "manifest.json" manifest.parent.mkdir(parents=True) manifest.write_text(json.dumps({"tests": [{"name": "case", "sources": ["a.f90"]}]}), encoding="utf-8") - with unittest.mock.patch.dict(os.environ, {}, clear=True): - with unittest.mock.patch.object(validate_fortran, "resolve_toolchain", return_value=None): + with mock.patch.dict(os.environ, {}, clear=True): + with mock.patch.object(validate_fortran, "resolve_toolchain", return_value=None): with self.assertRaises(validate_fortran.FortranValidationError): validate_fortran.discover_commands(root) @@ -71,8 +72,8 @@ class ValidateFortranTests(unittest.TestCase): encoding="utf-8", ) toolchain = validate_fortran.FortranToolchain(name="ifx", executable="ifx", env_script=None) - with unittest.mock.patch.dict(os.environ, {}, clear=True): - with unittest.mock.patch.object(validate_fortran, "resolve_toolchain", return_value=toolchain): + with mock.patch.dict(os.environ, {}, clear=True): + with mock.patch.object(validate_fortran, "resolve_toolchain", return_value=toolchain): commands = validate_fortran.discover_commands(root) self.assertEqual(len(commands), 2) @@ -81,6 +82,36 @@ class ValidateFortranTests(unittest.TestCase): self.assertIn("/exe:", commands[0]) self.assertTrue(commands[1].endswith("umat_linear_elastic_kernel.exe")) + def test_manifest_build_commands_create_test_build_directories(self): + validate_fortran = load_validate_fortran() + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + manifest = root / "tests" / "fortran" / "manifest.json" + manifest.parent.mkdir(parents=True) + manifest.write_text( + json.dumps( + { + "tests": [ + { + "name": "uel_3d_euler_beam_kernel_stiffness", + "sources": [ + "src/fortran/uel_3d_euler_beam_kernel.f90", + "tests/fortran/test_uel_3d_euler_beam_kernel.f90", + ], + } + ] + } + ), + encoding="utf-8", + ) + toolchain = validate_fortran.FortranToolchain(name="ifx", executable="ifx", env_script=None) + with mock.patch.dict(os.environ, {}, clear=True): + with mock.patch.object(validate_fortran, "resolve_toolchain", return_value=toolchain): + validate_fortran.discover_commands(root) + + build_dir = root / "build" / "fortran-tests" / "uel_3d_euler_beam_kernel_stiffness" + self.assertTrue(build_dir.is_dir()) + if __name__ == "__main__": unittest.main() diff --git a/scripts/validate_fortran.py b/scripts/validate_fortran.py index 55d0246..1e48147 100644 --- a/scripts/validate_fortran.py +++ b/scripts/validate_fortran.py @@ -62,6 +62,7 @@ def build_test_commands(root: Path, manifest: dict, toolchain: FortranToolchain) for record in manifest.get("tests", []): name, sources = _validate_test_record(record) build_dir = root / BUILD_ROOT / name + build_dir.mkdir(parents=True, exist_ok=True) exe_path = build_dir / f"{name}.exe" source_paths = [root / source for source in sources] compile_args: list[str | Path] = [ diff --git a/src/fortran/uel_3d_euler_beam_abi_adapter.f90 b/src/fortran/uel_3d_euler_beam_abi_adapter.f90 new file mode 100644 index 0000000..7601add --- /dev/null +++ b/src/fortran/uel_3d_euler_beam_abi_adapter.f90 @@ -0,0 +1,121 @@ +module uel_3d_euler_beam_abi_adapter + use iso_fortran_env, only: real64 + use ieee_arithmetic, only: ieee_is_finite + use uel_3d_euler_beam_kernel, only: & + UEL3DEB_OK, UEL3DEB_E001_NDOFEL, UEL3DEB_E002_NNODE, & + UEL3DEB_E003_MCRD, UEL3DEB_E004_NPROPS, UEL3DEB_E005_NJPROP, & + UEL3DEB_E006_NSVARS, UEL3DEB_E007_NRHS, UEL3DEB_E008_MLVARX, & + UEL3DEB_E009_NONFINITE, UEL3DEB_E014_LFLAGS2, & + UEL3DEB_E015_LFLAGS3, UEL3DEB_E016_NDLOAD, & + uel3deb_validate_kernel_inputs, uel3deb_global_stiffness + implicit none + + private + + integer, parameter :: dp = real64 + + public :: uel3deb_abi_static + +contains + + subroutine uel3deb_abi_static(rhs, amatrx, energy, ndofel, nrhs, nsvars, & + props, nprops, coords, mcrd, nnode, u, & + lflags, mlvarx, ndload, pnewdt, jprops, & + njprop, status) + real(dp), intent(inout) :: rhs(:, :) + real(dp), intent(inout) :: amatrx(:, :) + real(dp), intent(inout) :: energy(:) + integer, intent(in) :: ndofel + integer, intent(in) :: nrhs + integer, intent(in) :: nsvars + real(dp), intent(in) :: props(:) + integer, intent(in) :: nprops + real(dp), intent(in) :: coords(:, :) + integer, intent(in) :: mcrd + integer, intent(in) :: nnode + real(dp), intent(in) :: u(:) + integer, intent(in) :: lflags(:) + integer, intent(in) :: mlvarx + integer, intent(in) :: ndload + real(dp), intent(inout) :: pnewdt + integer, intent(in) :: jprops(:) + integer, intent(in) :: njprop + integer, intent(out) :: status + real(dp) :: coords3(3, 2) + real(dp) :: props9(9) + real(dp) :: u12(12) + real(dp) :: k_global(12, 12) + real(dp) :: transform(12, 12) + real(dp) :: residual(12) + + status = UEL3DEB_OK + rhs = 0.0_dp + amatrx = 0.0_dp + energy = 0.0_dp + + if (ndofel /= 12) then + status = UEL3DEB_E001_NDOFEL + return + end if + if (nnode /= 2) then + status = UEL3DEB_E002_NNODE + return + end if + if (mcrd < 3) then + status = UEL3DEB_E003_MCRD + return + end if + if (nprops /= 9) then + status = UEL3DEB_E004_NPROPS + return + end if + if (njprop /= 0) then + status = UEL3DEB_E005_NJPROP + return + end if + if (nsvars /= 0) then + status = UEL3DEB_E006_NSVARS + return + end if + if (nrhs /= 1) then + status = UEL3DEB_E007_NRHS + return + end if + if (mlvarx < 12) then + status = UEL3DEB_E008_MLVARX + return + end if + if (lflags(2) /= 0) then + status = UEL3DEB_E014_LFLAGS2 + return + end if + if (.not. any(lflags(3) == [1, 2, 5])) then + status = UEL3DEB_E015_LFLAGS3 + return + end if + if (ndload /= 0) then + status = UEL3DEB_E016_NDLOAD + return + end if + + props9 = props(1:9) + coords3 = coords(1:3, 1:2) + u12 = u(1:12) + + if (.not. all(ieee_is_finite(u12))) then + status = UEL3DEB_E009_NONFINITE + return + end if + + call uel3deb_validate_kernel_inputs(coords3, props9, status) + if (status /= UEL3DEB_OK) return + + call uel3deb_global_stiffness(coords3, props9, k_global, transform, status) + if (status /= UEL3DEB_OK) return + + residual = -matmul(k_global, u12) + if (lflags(3) == 1 .or. lflags(3) == 2) amatrx(1:12, 1:12) = k_global + if (lflags(3) == 1 .or. lflags(3) == 5) rhs(1:12, 1) = residual + end subroutine uel3deb_abi_static + +end module uel_3d_euler_beam_abi_adapter diff --git a/src/fortran/uel_3d_euler_beam_kernel.f90 b/src/fortran/uel_3d_euler_beam_kernel.f90 new file mode 100644 index 0000000..49c56b9 --- /dev/null +++ b/src/fortran/uel_3d_euler_beam_kernel.f90 @@ -0,0 +1,274 @@ +module uel_3d_euler_beam_kernel + use iso_fortran_env, only: real64 + use ieee_arithmetic, only: ieee_is_finite + implicit none + + private + + integer, parameter :: dp = real64 + + integer, parameter, public :: UEL3DEB_OK = 0 + integer, parameter, public :: UEL3DEB_E001_NDOFEL = 1 + integer, parameter, public :: UEL3DEB_E002_NNODE = 2 + integer, parameter, public :: UEL3DEB_E003_MCRD = 3 + integer, parameter, public :: UEL3DEB_E004_NPROPS = 4 + integer, parameter, public :: UEL3DEB_E005_NJPROP = 5 + integer, parameter, public :: UEL3DEB_E006_NSVARS = 6 + integer, parameter, public :: UEL3DEB_E007_NRHS = 7 + integer, parameter, public :: UEL3DEB_E008_MLVARX = 8 + integer, parameter, public :: UEL3DEB_E009_NONFINITE = 9 + integer, parameter, public :: UEL3DEB_E010_NONPOSITIVE_PROPERTY = 10 + integer, parameter, public :: UEL3DEB_E011_ZERO_LENGTH = 11 + integer, parameter, public :: UEL3DEB_E012_ZERO_ORIENTATION = 12 + integer, parameter, public :: UEL3DEB_E013_PARALLEL_ORIENTATION = 13 + integer, parameter, public :: UEL3DEB_E014_LFLAGS2 = 14 + integer, parameter, public :: UEL3DEB_E015_LFLAGS3 = 15 + integer, parameter, public :: UEL3DEB_E016_NDLOAD = 16 + + public :: uel3deb_validate_kernel_inputs + public :: uel3deb_local_stiffness + public :: uel3deb_global_stiffness + +contains + + subroutine uel3deb_validate_kernel_inputs(coords, props, status) + real(dp), intent(in) :: coords(3, 2) + real(dp), intent(in) :: props(9) + integer, intent(out) :: status + real(dp) :: axis(3) + real(dp) :: a_ref(3) + real(dp) :: a_perp(3) + real(dp) :: length + real(dp) :: coord_scale + real(dp) :: ref_norm + real(dp) :: perp_norm + + status = UEL3DEB_OK + + if (.not. all(ieee_is_finite(coords))) then + status = UEL3DEB_E009_NONFINITE + return + end if + if (.not. all(ieee_is_finite(props))) then + status = UEL3DEB_E009_NONFINITE + return + end if + if (any(props(1:6) <= 0.0_dp)) then + status = UEL3DEB_E010_NONPOSITIVE_PROPERTY + return + end if + + axis = coords(:, 2) - coords(:, 1) + length = norm3(axis) + coord_scale = max(1.0_dp, norm3(coords(:, 1)), norm3(coords(:, 2))) + if (length <= 1.0e-12_dp * coord_scale) then + status = UEL3DEB_E011_ZERO_LENGTH + return + end if + + a_ref = props(7:9) + ref_norm = norm3(a_ref) + if (ref_norm <= 1.0e-12_dp) then + status = UEL3DEB_E012_ZERO_ORIENTATION + return + end if + + axis = axis / length + a_perp = a_ref - dot_product(a_ref, axis) * axis + perp_norm = norm3(a_perp) + if (perp_norm / ref_norm <= 1.0e-8_dp) then + status = UEL3DEB_E013_PARALLEL_ORIENTATION + return + end if + end subroutine uel3deb_validate_kernel_inputs + + subroutine uel3deb_local_stiffness(length, young, shear, area, iy, iz, & + torsion_j, k_local, status) + real(dp), intent(in) :: length + real(dp), intent(in) :: young + real(dp), intent(in) :: shear + real(dp), intent(in) :: area + real(dp), intent(in) :: iy + real(dp), intent(in) :: iz + real(dp), intent(in) :: torsion_j + real(dp), intent(out) :: k_local(12, 12) + integer, intent(out) :: status + real(dp) :: axial + real(dp) :: torsion + real(dp) :: b2_12 + real(dp) :: b2_6 + real(dp) :: b2_4 + real(dp) :: b2_2 + real(dp) :: b3_12 + real(dp) :: b3_6 + real(dp) :: b3_4 + real(dp) :: b3_2 + + k_local = 0.0_dp + status = UEL3DEB_OK + + if (.not. ieee_is_finite(length) .or. .not. ieee_is_finite(young) .or. & + .not. ieee_is_finite(shear) .or. .not. ieee_is_finite(area) .or. & + .not. ieee_is_finite(iy) .or. .not. ieee_is_finite(iz) .or. & + .not. ieee_is_finite(torsion_j)) then + status = UEL3DEB_E009_NONFINITE + return + end if + if (young <= 0.0_dp .or. shear <= 0.0_dp .or. area <= 0.0_dp .or. & + iy <= 0.0_dp .or. iz <= 0.0_dp .or. torsion_j <= 0.0_dp) then + status = UEL3DEB_E010_NONPOSITIVE_PROPERTY + return + end if + if (length <= 0.0_dp) then + status = UEL3DEB_E011_ZERO_LENGTH + return + end if + + axial = young * area / length + torsion = shear * torsion_j / length + b2_12 = 12.0_dp * young * iy / length**3 + b2_6 = 6.0_dp * young * iy / length**2 + b2_4 = 4.0_dp * young * iy / length + b2_2 = 2.0_dp * young * iy / length + b3_12 = 12.0_dp * young * iz / length**3 + b3_6 = 6.0_dp * young * iz / length**2 + b3_4 = 4.0_dp * young * iz / length + b3_2 = 2.0_dp * young * iz / length + + k_local(1, 1) = axial + k_local(1, 7) = -axial + k_local(7, 1) = -axial + k_local(7, 7) = axial + + k_local(4, 4) = torsion + k_local(4, 10) = -torsion + k_local(10, 4) = -torsion + k_local(10, 10) = torsion + + k_local(3, 3) = b2_12 + k_local(3, 5) = -b2_6 + k_local(3, 9) = -b2_12 + k_local(3, 11) = -b2_6 + k_local(5, 3) = -b2_6 + k_local(5, 5) = b2_4 + k_local(5, 9) = b2_6 + k_local(5, 11) = b2_2 + k_local(9, 3) = -b2_12 + k_local(9, 5) = b2_6 + k_local(9, 9) = b2_12 + k_local(9, 11) = b2_6 + k_local(11, 3) = -b2_6 + k_local(11, 5) = b2_2 + k_local(11, 9) = b2_6 + k_local(11, 11) = b2_4 + + k_local(2, 2) = b3_12 + k_local(2, 6) = b3_6 + k_local(2, 8) = -b3_12 + k_local(2, 12) = b3_6 + k_local(6, 2) = b3_6 + k_local(6, 6) = b3_4 + k_local(6, 8) = -b3_6 + k_local(6, 12) = b3_2 + k_local(8, 2) = -b3_12 + k_local(8, 6) = -b3_6 + k_local(8, 8) = b3_12 + k_local(8, 12) = -b3_6 + k_local(12, 2) = b3_6 + k_local(12, 6) = b3_2 + k_local(12, 8) = -b3_6 + k_local(12, 12) = b3_4 + end subroutine uel3deb_local_stiffness + + subroutine uel3deb_global_stiffness(coords, props, k_global, transform, status) + real(dp), intent(in) :: coords(3, 2) + real(dp), intent(in) :: props(9) + real(dp), intent(out) :: k_global(12, 12) + real(dp), intent(out) :: transform(12, 12) + integer, intent(out) :: status + real(dp) :: k_local(12, 12) + real(dp) :: rotation(3, 3) + real(dp) :: length + + k_global = 0.0_dp + transform = 0.0_dp + + call build_frame(coords, props, rotation, length, status) + if (status /= UEL3DEB_OK) return + + call uel3deb_local_stiffness(length, props(1), props(2), props(3), & + props(4), props(5), props(6), k_local, & + status) + if (status /= UEL3DEB_OK) return + + call fill_transform(rotation, transform) + k_global = matmul(transpose(transform), matmul(k_local, transform)) + end subroutine uel3deb_global_stiffness + + subroutine build_frame(coords, props, rotation, length, status) + real(dp), intent(in) :: coords(3, 2) + real(dp), intent(in) :: props(9) + real(dp), intent(out) :: rotation(3, 3) + real(dp), intent(out) :: length + integer, intent(out) :: status + real(dp) :: e1(3) + real(dp) :: e2(3) + real(dp) :: e3(3) + real(dp) :: a_ref(3) + real(dp) :: a_perp(3) + + rotation = 0.0_dp + length = 0.0_dp + + call uel3deb_validate_kernel_inputs(coords, props, status) + if (status /= UEL3DEB_OK) return + + e1 = coords(:, 2) - coords(:, 1) + length = norm3(e1) + e1 = e1 / length + + a_ref = props(7:9) + a_perp = a_ref - dot_product(a_ref, e1) * e1 + e2 = a_perp / norm3(a_perp) + e3 = cross3(e1, e2) + + rotation(1, :) = e1 + rotation(2, :) = e2 + rotation(3, :) = e3 + end subroutine build_frame + + subroutine fill_transform(rotation, transform) + real(dp), intent(in) :: rotation(3, 3) + real(dp), intent(out) :: transform(12, 12) + integer :: block + integer :: i + integer :: j + + transform = 0.0_dp + do block = 0, 3 + do i = 1, 3 + do j = 1, 3 + transform(3 * block + i, 3 * block + j) = rotation(i, j) + end do + end do + end do + end subroutine fill_transform + + pure function norm3(values) result(length) + real(dp), intent(in) :: values(3) + real(dp) :: length + + length = sqrt(dot_product(values, values)) + end function norm3 + + pure function cross3(a, b) result(c) + real(dp), intent(in) :: a(3) + real(dp), intent(in) :: b(3) + real(dp) :: c(3) + + c(1) = a(2) * b(3) - a(3) * b(2) + c(2) = a(3) * b(1) - a(1) * b(3) + c(3) = a(1) * b(2) - a(2) * b(1) + end function cross3 + +end module uel_3d_euler_beam_kernel diff --git a/tests/fortran/manifest.json b/tests/fortran/manifest.json index 9683d2a..1f6e410 100644 --- a/tests/fortran/manifest.json +++ b/tests/fortran/manifest.json @@ -33,6 +33,23 @@ "src/fortran/uel_3d_euler_beam_abi_adapter.f90", "tests/fortran/uel_3d_euler_beam/test_invalid_inputs.f90" ] + }, + { + "name": "uel_3d_euler_beam_kernel_source_smoke", + "sources": [ + "tests/fortran/uel_3d_euler_beam/test_support.f90", + "src/fortran/uel_3d_euler_beam_kernel.f90", + "tests/fortran/test_uel_3d_euler_beam_kernel.f90" + ] + }, + { + "name": "uel_3d_euler_beam_abi_adapter_source_smoke", + "sources": [ + "tests/fortran/uel_3d_euler_beam/test_support.f90", + "src/fortran/uel_3d_euler_beam_kernel.f90", + "src/fortran/uel_3d_euler_beam_abi_adapter.f90", + "tests/fortran/test_uel_3d_euler_beam_abi_adapter.f90" + ] } ] } diff --git a/tests/fortran/test_uel_3d_euler_beam_abi_adapter.f90 b/tests/fortran/test_uel_3d_euler_beam_abi_adapter.f90 new file mode 100644 index 0000000..fb72625 --- /dev/null +++ b/tests/fortran/test_uel_3d_euler_beam_abi_adapter.f90 @@ -0,0 +1,33 @@ +program test_uel_3d_euler_beam_abi_adapter + use uel_3d_euler_beam_test_support + use uel_3d_euler_beam_kernel, only: UEL3DEB_OK + use uel_3d_euler_beam_abi_adapter, only: uel3deb_abi_static + implicit none + + real(dp) :: rhs(12, 1) + real(dp) :: amatrx(12, 12) + real(dp) :: energy(8) + real(dp) :: props(9) + real(dp) :: coords(3, 2) + real(dp) :: u(12) + real(dp) :: pnewdt + integer :: lflags(5) + integer :: jprops(1) + integer :: status + + coords(:, 1) = [0.0_dp, 0.0_dp, 0.0_dp] + coords(:, 2) = [2.0_dp, 0.0_dp, 0.0_dp] + props = [210.0e9_dp, 80.0e9_dp, 3.0e-3_dp, 4.0e-6_dp, 7.0e-6_dp, & + 2.5e-6_dp, 0.0_dp, 1.0_dp, 0.0_dp] + u = 0.0_dp + pnewdt = 1.0_dp + lflags = 0 + lflags(3) = 2 + jprops = 0 + + call uel3deb_abi_static(rhs, amatrx, energy, 12, 1, 0, props, 9, coords, & + 3, 2, u, lflags, 12, 0, pnewdt, jprops, 0, status) + call assert_equal_int(status, UEL3DEB_OK, 'abi adapter source smoke status') + call assert_matrix_symmetric(amatrx, tol_symmetry, 'abi adapter source smoke K') + write(*, '(A)') 'PASS uel_3d_euler_beam_abi_adapter_source_smoke' +end program test_uel_3d_euler_beam_abi_adapter diff --git a/tests/fortran/test_uel_3d_euler_beam_kernel.f90 b/tests/fortran/test_uel_3d_euler_beam_kernel.f90 new file mode 100644 index 0000000..f8f499a --- /dev/null +++ b/tests/fortran/test_uel_3d_euler_beam_kernel.f90 @@ -0,0 +1,19 @@ +program test_uel_3d_euler_beam_kernel + use uel_3d_euler_beam_test_support + use uel_3d_euler_beam_kernel, only: & + UEL3DEB_OK, uel3deb_validate_kernel_inputs + implicit none + + real(dp) :: coords(3, 2) + real(dp) :: props(9) + integer :: status + + coords(:, 1) = [0.0_dp, 0.0_dp, 0.0_dp] + coords(:, 2) = [2.0_dp, 0.0_dp, 0.0_dp] + props = [210.0e9_dp, 80.0e9_dp, 3.0e-3_dp, 4.0e-6_dp, 7.0e-6_dp, & + 2.5e-6_dp, 0.0_dp, 1.0_dp, 0.0_dp] + + call uel3deb_validate_kernel_inputs(coords, props, status) + call assert_equal_int(status, UEL3DEB_OK, 'kernel source smoke status') + write(*, '(A)') 'PASS uel_3d_euler_beam_kernel_source_smoke' +end program test_uel_3d_euler_beam_kernel