diff --git a/.agents/skills/commit-format/SKILL.md b/.agents/skills/commit-format/SKILL.md new file mode 100644 index 000000000..fb9ac3e40 --- /dev/null +++ b/.agents/skills/commit-format/SKILL.md @@ -0,0 +1,41 @@ +--- +name: commit-format +description: Provides this repository's required commit-message format, categories, and examples. Use when creating, reviewing, or proposing git commit messages for FDTD. +license: MIT +metadata: + author: Elemwave + version: "1.0" +--- + +When creating or reviewing commit messages, follow this workflow: + +1. **Identify change layer**: Choose category for innermost layer changed. Use `Domain` for pure business logic, `ElemData` for data models and utilities, `Command` for FreeCAD commands and data containers, `Mapper` for domain-to-FreeCAD mappings, `Service` for export/validation/preferences, `Util` for FreeCAD utilities, `View` for task panels and widgets, `Test` for tests, `Refactor` for non-functional reorganisations, `CI` for automation, or `APIWrapper` for the SEMBA CLI interface. Use actual module name, such as `QtWrapper`, `Extensions`, or `Init`, if no category fits. + +2. **Split changes semantically**: Each commit must be atomic: one logical, cohesive change. Cross-layer changes should be split where practical. A Domain model plus Command change normally becomes one `Domain` commit and one `Command` commit. + +3. **Write repository commit message**: Use exact format `FDTD | | # | `. `FDTD` is literal. Include GitHub issue number, such as `#866`. +Allowed types + - `feat`: new feature + - `fix`: bug fix + - `refactor`: code restructuring without behavior change + - `docs`: documentation only + - `style`: formatting, whitespace, semicolons (no logic change) + - `test`: adding or updating tests + - `chore`: tooling, configs, dependencies + - `perf`: performance improvement + - `ci`: CI/CD changes + - `build`: build system changes + - `revert`: reverting a previous commit +Write description in imperative mood, concise, and ideally 72 characters or fewer. + - Good: `FDTD | chore | #866 | Use relative path for STEP file in TULIP JSON export` + - Good: `FDTD | feat | #880 | Add validation for solver parameter ranges` + - Bad: `fix(service): use relative path for STEP file` + +4. **Apply commit rules**: + - Use one-line messages only. Never use a commit body. + - Never include trailers, `Co-Authored-By`, AI/tool attribution, generated-by markers, bot signatures, or AI metadata. + - Preserve capitalisation for `FDTD`, categories, proper nouns, and identifiers. + - Use `git add` with specific files for each commit. Do not use `git add .` unless all changes belong to same commit. + - After each commit, run `git log -1 --format=%B` and verify message has no prohibited attribution or metadata. Then run `git status` to verify success. + +5. **Execution order**: Stage and commit one semantic group at a time. Do not skip ahead. diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 6c632061b..000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -FROM intel/oneapi-hpckit \ No newline at end of file diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 000000000..0500829a6 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "1.8.0", + "resolved": "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511", + "integrity": "sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511" + }, + "ghcr.io/msclock/features/vcpkg:2": { + "version": "2.0.0", + "resolved": "ghcr.io/msclock/features/vcpkg@sha256:bcb75d475252af1f9ef742860387cb15e059f57041748abdd02030cd1a181471", + "integrity": "sha256:bcb75d475252af1f9ef742860387cb15e059f57041748abdd02030cd1a181471" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bed4a83e0..012b6c989 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,46 +1,49 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/cpp { - "name": "OpenSEMBA dev. framework", - "build": { - "dockerfile": "Dockerfile" - }, - "features": { - "ghcr.io/msclock/features/vcpkg:2": {}, - "ghcr.io/devcontainers/features/python:1": {} - }, - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - - // Configure tool-specific properties. - "customizations": { - "codespaces": { - "repositories": { - "lmdiazangulo/json-fortran": { "permissions": "read-all" }, - "opensemba/fhash": { "permissions": "read-all" }, - "reference-lapack/lapack": { "permissions": "read-all" }, - "opensemba/ngtest": {"permissions": "read-all" }, - "google/googletest": { "permissions": "read-all" } - } - }, - "vscode": { - "extensions": [ - "ms-toolsai.jupyter", - "fortran-lang.linter-gfortran", - "ms-vscode.cmake-tools", - "ms-python.autopep8", - "matepek.vscode-catch2-test-adapter" - ] - } - } - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + "name": "semba-fdtd dev", + "dockerComposeFile": [ + "../docker-compose.yml" + ], + "service": "dev", + "workspaceFolder": "/home/developer/workspaces/fdtd", + "remoteUser": "developer", + "containerUser": "developer", + "overrideCommand": false, + "features": { + "ghcr.io/msclock/features/vcpkg:2": {}, + "ghcr.io/devcontainers/features/python:1": {} + }, + "remoteEnv": { + "HOME": "/home/developer", + "XDG_CONFIG_HOME": "/home/developer/.config", + "XDG_DATA_HOME": "/home/developer/.local/share", + "OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}" + }, + "postCreateCommand": ".devcontainer/setup-python.sh", + "postStartCommand": "bash scripts/devcontainer-post-start.sh", + "customizations": { + "codespaces": { + "repositories": { + "lmdiazangulo/json-fortran": { "permissions": "read-all" }, + "opensemba/fhash": { "permissions": "read-all" }, + "reference-lapack/lapack": { "permissions": "read-all" }, + "opensemba/ngtest": { "permissions": "read-all" }, + "google/googletest": { "permissions": "read-all" } + } + }, + "vscode": { + "extensions": [ + "fortran-lang.linter-gfortran", + "ms-vscode.cmake-tools", + "ms-vscode.cpptools", + "ms-python.python", + "mhutchie.git-graph" + ], + "settings": { + "cmake.buildDirectory": "${workspaceFolder}/build", + "cmake.useCMakePresets": "always", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "terminal.integrated.defaultProfile.linux": "bash" + } + } + } } diff --git a/.devcontainer/intel/devcontainer-lock.json b/.devcontainer/intel/devcontainer-lock.json new file mode 100644 index 000000000..0500829a6 --- /dev/null +++ b/.devcontainer/intel/devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "1.8.0", + "resolved": "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511", + "integrity": "sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511" + }, + "ghcr.io/msclock/features/vcpkg:2": { + "version": "2.0.0", + "resolved": "ghcr.io/msclock/features/vcpkg@sha256:bcb75d475252af1f9ef742860387cb15e059f57041748abdd02030cd1a181471", + "integrity": "sha256:bcb75d475252af1f9ef742860387cb15e059f57041748abdd02030cd1a181471" + } + } +} diff --git a/.devcontainer/intel/devcontainer.json b/.devcontainer/intel/devcontainer.json new file mode 100644 index 000000000..8a3004d57 --- /dev/null +++ b/.devcontainer/intel/devcontainer.json @@ -0,0 +1,57 @@ +{ + "name": "semba-fdtd Intel dev", + "dockerComposeFile": [ + "../../docker-compose.yml" + ], + "service": "intel-dev", + "workspaceFolder": "/home/developer/workspaces/fdtd", + "remoteUser": "developer", + "containerUser": "developer", + "overrideCommand": false, + "features": { + "ghcr.io/msclock/features/vcpkg:2": {}, + "ghcr.io/devcontainers/features/python:1": {} + }, + "remoteEnv": { + "HOME": "/home/developer", + "XDG_CONFIG_HOME": "/home/developer/.config", + "XDG_DATA_HOME": "/home/developer/.local/share", + "OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}" + }, + "postCreateCommand": ".devcontainer/setup-python.sh", + "postStartCommand": "bash scripts/devcontainer-post-start.sh", + "customizations": { + "codespaces": { + "repositories": { + "lmdiazangulo/json-fortran": { "permissions": "read-all" }, + "opensemba/fhash": { "permissions": "read-all" }, + "reference-lapack/lapack": { "permissions": "read-all" }, + "opensemba/ngtest": { "permissions": "read-all" }, + "google/googletest": { "permissions": "read-all" } + } + }, + "vscode": { + "extensions": [ + "fortran-lang.linter-gfortran", + "ms-vscode.cmake-tools", + "ms-vscode.cpptools", + "ms-python.python", + "mhutchie.git-graph", + "matepek.vscode-catch2-test-adapter" + ], + "settings": { + "cmake.buildDirectory": "${workspaceFolder}/build-intel-dbg", + "cmake.configurePreset": "intel-dbg", + "cmake.buildPreset": "intel-dbg", + "cmake.useCMakePresets": "always", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "terminal.integrated.defaultProfile.linux": "bash", + "fortran.fortls.path": "/usr/local/bin/fortls", + "fortran.fortls.notifyInit": true, + "fortran.fortls.disableAutoupdate": true, + "fortran.provide.hover": "Both", + "fortran.formatting.formatter": "fprettify" + } + } + } +} diff --git a/.devcontainer/notes.md b/.devcontainer/notes.md deleted file mode 100644 index 26d157f49..000000000 --- a/.devcontainer/notes.md +++ /dev/null @@ -1,12 +0,0 @@ - -``` -git submodule init -git submodule --recursive -``` - -Install python packages -``` -python3 -m venv ~/py_envs -source ~/py_envs/bin/activate -python3 -m pip install -r requirements.txt -``` \ No newline at end of file diff --git a/.devcontainer/setup-python.sh b/.devcontainer/setup-python.sh new file mode 100755 index 000000000..5912f1780 --- /dev/null +++ b/.devcontainer/setup-python.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +python3 -m venv .venv +.venv/bin/python -m pip install --upgrade pip +.venv/bin/python -m pip install -r requirements.txt diff --git a/.dockerignore b/.dockerignore index be16480fc..620187183 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,10 @@ .git build/ +build-*/ .venv/ +.devcontainer/ +.vscode/ +.github/ __pycache__/ *.pyc *.pyo @@ -9,4 +13,18 @@ __pycache__/ dist/ *.o *.mod -# Keep precompiled_libraries and external/ submodule contents + +# Runtime/development bind mounts do not need to be copied into image layers. +simulations/ + +# The top-level build only consumes selected external sources. +external/lapack/ +external/ngspice/examples/ +external/ngspice/tests/ +external/ngspice/visualc/ +external/ngspice/man/ +external/ngspice/doc/ +external/ngspice/.git/ +external/googletest/.git/ +external/json-fortran/.git/ +external/fhash/.git/ diff --git a/.github/workflows/automatic-release-ubuntu-gnu.yml b/.github/workflows/automatic-release-ubuntu-gnu.yml index 12d9187b6..c21999662 100644 --- a/.github/workflows/automatic-release-ubuntu-gnu.yml +++ b/.github/workflows/automatic-release-ubuntu-gnu.yml @@ -3,54 +3,41 @@ name: automatic-release-ubuntu-gnu on: push: branches: - - main + - main jobs: - - builds-and-test: + build-release: runs-on: ubuntu-latest strategy: - matrix: + matrix: build-type: ["Release"] - mpi: ["OFF"] mtln: ["ON"] - hdf: ["ON"] - - steps: + include: + - mpi: "OFF" + suffix: "" + title-suffix: "" + - mpi: "ON" + suffix: "-mpi" + title-suffix: " MPI" + + steps: - name: Checkout uses: actions/checkout@v4 with: submodules: 'recursive' - + - name: Install required packages run: | sudo apt update sudo apt install libhdf5-dev libopenmpi-dev - + - name: CMake build run: | cmake -S . -B build \ - -DCMAKE_BUILD_TYPE=${{matrix.build-type}} \ - -DSEMBA_FDTD_ENABLE_MPI=${{matrix.mpi}} \ - -DSEMBA_FDTD_ENABLE_HDF=${{matrix.hdf}} \ - -DSEMBA_FDTD_ENABLE_MTLN=${{matrix.mtln}} + -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ + -DSEMBA_FDTD_ENABLE_MPI=${{ matrix.mpi }} \ + -DSEMBA_FDTD_ENABLE_MTLN=${{ matrix.mtln }} cmake --build build -j - - - name: Run fdtd unit tests - run: | - build/bin/fdtd_tests - - - name: Install python wrapper requirements - run: | - python -m pip install -r requirements.txt - - - name: Run wrapper tests - env: - SEMBA_FDTD_ENABLE_MPI: ${{ matrix.mpi }} - SEMBA_FDTD_ENABLE_MTLN: ${{ matrix.mtln }} - SEMBA_FDTD_ENABLE_HDF: ${{ matrix.hdf }} - run: | - python -m pytest test - name: Get current date id: date @@ -58,14 +45,14 @@ jobs: - name: Creating archive run: | - tar -czvf semba-fdtd-ubuntu-gnu.tar.gz build/bin/* LICENSE + tar -czvf semba-fdtd-ubuntu-gnu${{ matrix.suffix }}.tar.gz build/bin/* LICENSE - name: Generating release uses: "marvinpinto/action-automatic-releases@latest" with: repo_token: "${{ secrets.GITHUB_TOKEN }}" - automatic_release_tag: "${{ steps.date.outputs.date }}-ubuntu-gnu" + automatic_release_tag: "${{ steps.date.outputs.date }}-ubuntu-gnu${{ matrix.suffix }}" prerelease: false - title: "${{ steps.date.outputs.date }} Ubuntu GNU dev. build" + title: "${{ steps.date.outputs.date }} Ubuntu GNU${{ matrix.title-suffix }} dev. build" files: | - semba-fdtd-ubuntu-gnu.tar.gz + semba-fdtd-ubuntu-gnu${{ matrix.suffix }}.tar.gz diff --git a/.github/workflows/automatic-release-windows-intelLLVM.yml b/.github/workflows/automatic-release-windows-intelLLVM.yml index 4abb0394e..001c235c2 100644 --- a/.github/workflows/automatic-release-windows-intelLLVM.yml +++ b/.github/workflows/automatic-release-windows-intelLLVM.yml @@ -14,7 +14,6 @@ jobs: build-type: ["Release"] mpi: ["OFF"] mtln: ["ON"] - hdf: ["ON"] steps: @@ -32,35 +31,66 @@ jobs: id: setup-fortran with: compiler: intel - version: '2025.2.1' - + version: '2025.2.1' + + # ifx requires MSVC's linker and Windows SDK environment on the hosted runner. + - name: Setup MSVC toolchain + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + - name: Setup ninja uses: seanmiddleditch/gha-setup-ninja@master + # CMake 4.4 cannot detect the ifx 2025.2 Fortran preprocessor on Windows. + - name: Setup CMake + shell: pwsh + run: | + $archive = "$env:RUNNER_TEMP\cmake-3.31.6-windows-x86_64.zip" + $directory = "$env:RUNNER_TEMP\cmake-3.31.6-windows-x86_64" + Invoke-WebRequest -Uri https://cmake.org/files/v3.31/cmake-3.31.6-windows-x86_64.zip -OutFile $archive + Expand-Archive -Path $archive -DestinationPath $env:RUNNER_TEMP + "$directory\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: CMake build shell: bash run: | cmake -S . -B build -G Ninja \ -DCMAKE_BUILD_TYPE=${{matrix.build-type}} \ -DSEMBA_FDTD_ENABLE_MPI=${{matrix.mpi}} \ - -DSEMBA_FDTD_ENABLE_HDF=${{matrix.hdf}} \ - -DSEMBA_FDTD_ENABLE_MTLN=${{matrix.mtln}} + -DSEMBA_FDTD_ENABLE_MTLN=${{matrix.mtln}} \ + -DHDF5_DIR="$PWD/precompiled_libraries/windows-intel/hdf5/cmake" \ + -DHDF5_USE_STATIC_LIBRARIES=ON cmake --build build -j - name: Run fdtd unit tests run: | build/bin/fdtd_tests.exe - + - name: Install python wrapper requirements run: | python -m pip install -r requirements.txt + - name: Run HDF movie smoke test + run: python -m pytest test/pyWrapper/test_full_system.py::test_movie_in_planewave_in_box --basetemp=build/hdf-smoke + - name: Run all wrapper tests (except codemodel) env: SEMBA_FDTD_ENABLE_MPI: ${{ matrix.mpi }} SEMBA_FDTD_ENABLE_MTLN: ${{ matrix.mtln }} - SEMBA_FDTD_ENABLE_HDF: ${{ matrix.hdf }} - run: python -m pytest -m 'not codemodel' test/ + run: python -m pytest -m 'not codemodel' test/ --deselect=test/pyWrapper/test_full_system.py::test_movie_in_planewave_in_box --basetemp=build/pytest-tmp + + - name: Upload failed HDF output diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: hdf-output-${{ matrix.mtln }} + path: | + build/hdf-smoke/** + build/pytest-tmp/**/*.h5 + build/pytest-tmp/**/*.xdmf + build/pytest-tmp/**/*.json + if-no-files-found: warn - name: Get current date id: date @@ -79,4 +109,3 @@ jobs: title: "${{ steps.date.outputs.date }} Windows Intel LLVM dev. build" files: | semba-fdtd-windows-intelLLVM.tar.gz - diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 22c8b8df0..afbfee190 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -6,9 +6,6 @@ on: - main pull_request: - branches: - - main - - dev concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -22,36 +19,27 @@ jobs: os: [ubuntu-latest] compiler: [ {name: 'intel', version: '2025.1'}, + # Match the HDF5 Fortran ABI used by the Ubuntu packages. {name: 'gcc', version: 11} ] build-type: ["Release"] mpi: ["ON", "OFF"] mtln: ["ON", "OFF"] - hdf: ["ON"] - double-precision: ["OFF"] + double-precision: ["ON", "OFF"] - include: + #include: # Disable by lack of space on github action # - os: ubuntu-latest # compiler: {name: 'nvidia-hpc', version: '24.5'} # build-type: "Release" # mpi: "OFF" # mtln: "OFF" - # hdf: "OFF" # double-precision: "OFF" - - - os: ubuntu-latest # This is the only test with double precision. - compiler: {name: 'intel', version: '2025.1'} - build-type: "Release" - mpi: "ON" - mtln: "OFF" - hdf: "ON" - double-precision: "ON" fail-fast: false runs-on: ${{ matrix.os }} - name: ${{matrix.os}} / ${{matrix.compiler.name}} / ${{matrix.build-type}}-mpi(${{matrix.mpi}})-mtln(${{matrix.mtln}})-hdf(${{matrix.hdf}})-double(${{matrix.double-precision}}) + name: ${{matrix.os}} / ${{matrix.compiler.name}} / ${{matrix.build-type}}-mpi(${{matrix.mpi}})-mtln(${{matrix.mtln}})-double(${{matrix.double-precision}}) steps: - name: Checkout @@ -59,48 +47,122 @@ jobs: with: submodules: 'recursive' - + - name: Apt update + run: sudo apt update + - name: Install python wrapper requirements run: python -m pip install -r requirements.txt - - name: Setup MPI + - name: Setup intel MPI if: matrix.mpi=='ON' && matrix.compiler.name=='intel' uses: mpi4py/setup-mpi@v1 with: mpi: 'intelmpi' - - name: Install gcc required packages + - name: Install gcc MPI + if: matrix.mpi=='ON' && matrix.compiler.name=='gcc' + run: sudo apt install libhdf5-openmpi-dev libopenmpi-dev + + - name: Install gcc required hdf5 packages if: matrix.compiler.name=='gcc' - run: | - sudo apt update - sudo apt install libhdf5-dev libopenmpi-dev - + run: sudo apt install libhdf5-dev + - name: Setup fortran compiler uses: fortran-lang/setup-fortran@v1 id: setup-fortran with: compiler: ${{matrix.compiler.name}} version: ${{matrix.compiler.version}} + + - name: Setup ninja + uses: seanmiddleditch/gha-setup-ninja@master - name: Build application + id: build run: | - cmake -S . -B build \ + cmake -S . -B build -G Ninja\ -DCMAKE_BUILD_TYPE=${{matrix.build-type}} \ -DSEMBA_FDTD_ENABLE_MPI=${{matrix.mpi}} \ - -DSEMBA_FDTD_ENABLE_HDF=${{matrix.hdf}} \ -DSEMBA_FDTD_ENABLE_MTLN=${{matrix.mtln}} \ -DSEMBA_FDTD_ENABLE_DOUBLE_PRECISION=${{matrix.double-precision}} cmake --build build -j - - name: Run unit tests - run: build/bin/fdtd_tests + - name: Run ctest + id: ctest + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + continue-on-error: true + env: + # The hosted runner exposes fewer cores than the MPI integration tests use. + OMPI_MCA_rmaps_base_oversubscribe: ${{ matrix.compiler.name == 'gcc' && '1' || '' }} + run: ctest --test-dir build --output-on-failure - name: Run python tests + id: python-tests + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + continue-on-error: true env: SEMBA_FDTD_ENABLE_MPI: ${{ matrix.mpi }} SEMBA_FDTD_ENABLE_MTLN: ${{ matrix.mtln }} - SEMBA_FDTD_ENABLE_HDF: ${{ matrix.hdf }} - run: python -m pytest test/ --durations=20 + run: | + set -o pipefail + python -m pytest -vv test/ --durations=0 --junitxml=build/pytest-results.xml 2>&1 | tee build/pytest-duration.log + + - name: Upload pytest duration log + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + uses: actions/upload-artifact@v4 + with: + name: pytest-duration-log-${{ matrix.compiler.name }}-mpi-${{ matrix.mpi }}-mtln-${{ matrix.mtln }}-double-${{ matrix.double-precision }} + path: build/pytest-duration.log + if-no-files-found: warn + + - name: Upload failed test diagnostics + if: ${{ !cancelled() && (steps.ctest.outcome == 'failure' || steps.python-tests.outcome == 'failure') }} + uses: actions/upload-artifact@v4 + with: + name: test-diagnostics-${{ matrix.compiler.name }}-mpi-${{ matrix.mpi }}-mtln-${{ matrix.mtln }}-double-${{ matrix.double-precision }} + path: build/pytest-results.xml + if-no-files-found: warn + + - name: Report test failures + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + run: | + test "${{ steps.ctest.outcome }}" = success + test "${{ steps.python-tests.outcome }}" = success + + parallel-hdf5-build: + runs-on: ubuntu-latest + name: ubuntu / gcc / parallel-hdf5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Install parallel HDF5 + run: | + sudo apt update + sudo apt install gfortran-11 libhdf5-openmpi-dev libopenmpi-dev python3-h5py python3-numpy libhdf5-dev + + - name: Configure parallel HDF5 output + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_Fortran_COMPILER=gfortran-11 \ + -DSEMBA_FDTD_ENABLE_MPI=ON \ + -DSEMBA_FDTD_ENABLE_MTLN=OFF \ + -DSEMBA_FDTD_ENABLE_TEST=OFF \ + -DXDMF_HDF5_BUILD_TESTING=ON \ + -DHDF5_ROOT=/usr/lib/x86_64-linux-gnu/hdf5/openmpi + + - name: Build parallel HDF5 output + run: cmake --build build -j + + - name: Test parallel HDF5 output + env: + # The test uses three ranks, while the hosted runner provides two slots. + OMPI_MCA_rmaps_base_oversubscribe: 1 + run: ctest --test-dir build --output-on-failure -R '^xdmf-hdf5-(generate|verify|mpi-generate|mpi-verify)$' diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 4fbb54616..6ab6fd777 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -7,9 +7,6 @@ on: - main pull_request: - branches: - - main - - dev concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -18,37 +15,35 @@ concurrency: jobs: builds-and-tests: - strategy: matrix: os: [windows-latest] - compiler: [ {name: 'intel', version: '2025.2.1'} ] + compiler: [ + {name: 'intel', version: '2025.2.1'} + ] build-type: ["Release"] mpi: ["OFF"] mtln: ["ON", "OFF"] - hdf: ["ON"] double-precision: ["ON", "OFF"] fail-fast: false runs-on: ${{matrix.os}} - name: ${{matrix.os}} / ${{matrix.compiler.name}} / ${{matrix.build-type}}-mpi(${{matrix.mpi}})-mtln(${{matrix.mtln}})-hdf(${{matrix.hdf}})-double(${{matrix.double-precision}}) + name: ${{matrix.os}} / ${{matrix.compiler.name}} / ${{matrix.build-type}}-mpi(${{matrix.mpi}})-mtln(${{matrix.mtln}})-double(${{matrix.double-precision}}) steps: - - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 with: submodules: 'recursive' - name: Install python wrapper requirements - run: | - python -m pip install -r requirements.txt - - - name: Setup MPI - uses: mpi4py/setup-mpi@v1 - with: - mpi: 'intelmpi' + run: python -m pip install -r requirements.txt + #- name: Setup MPI + # uses: mpi4py/setup-mpi@v1 + # with: + # mpi: 'intelmpi' - name: Setup intel fortran uses: fortran-lang/setup-fortran@v1 @@ -57,27 +52,82 @@ jobs: compiler: ${{ matrix.compiler.name }} version: ${{ matrix.compiler.version }} + # ifx requires MSVC's linker and Windows SDK environment on the hosted runner. + - name: Setup MSVC toolchain + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + - name: Setup ninja uses: seanmiddleditch/gha-setup-ninja@master + # CMake 4.4 cannot detect the ifx 2025.2 Fortran preprocessor on Windows. + - name: Setup CMake + shell: pwsh + run: | + $archive = "$env:RUNNER_TEMP\cmake-3.31.6-windows-x86_64.zip" + $directory = "$env:RUNNER_TEMP\cmake-3.31.6-windows-x86_64" + Invoke-WebRequest -Uri https://cmake.org/files/v3.31/cmake-3.31.6-windows-x86_64.zip -OutFile $archive + Expand-Archive -Path $archive -DestinationPath $env:RUNNER_TEMP + "$directory\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: CMake build shell: bash run: | cmake -S . -B build -G Ninja \ -DCMAKE_BUILD_TYPE=${{matrix.build-type}} \ -DSEMBA_FDTD_ENABLE_MPI=${{matrix.mpi}} \ - -DSEMBA_FDTD_ENABLE_HDF=${{matrix.hdf}} \ -DSEMBA_FDTD_ENABLE_MTLN=${{matrix.mtln}} \ - -DSEMBA_FDTD_ENABLE_DOUBLE_PRECISION=${{matrix.double-precision}} + -DSEMBA_FDTD_ENABLE_DOUBLE_PRECISION=${{matrix.double-precision}} \ + -DHDF5_DIR="$PWD/precompiled_libraries/windows-intel/hdf5/cmake" \ + -DHDF5_USE_STATIC_LIBRARIES=ON cmake --build build -j - - name: Run unit tests - run: build/bin/fdtd_tests.exe + - name: Run ctest + id: ctest + continue-on-error: true + run: ctest --test-dir build --output-on-failure + + - name: Run HDF movie smoke test + id: hdf-smoke + continue-on-error: true + run: python -m pytest test/pyWrapper/test_full_system.py::test_movie_in_planewave_in_box --basetemp=build/hdf-smoke - name: Run python tests (except codemodel) + id: python-tests + continue-on-error: true env: SEMBA_FDTD_ENABLE_MPI: ${{ matrix.mpi }} SEMBA_FDTD_ENABLE_MTLN: ${{ matrix.mtln }} - SEMBA_FDTD_ENABLE_HDF: ${{ matrix.hdf }} - run: python -m pytest -m 'not codemodel' test/ --durations=20 + run: | + python -m pytest -m 'not codemodel' test/ --deselect=test/pyWrapper/test_full_system.py::test_movie_in_planewave_in_box --durations=0 --junitxml=build/pytest-results.xml --basetemp=build/pytest-tmp 2>&1 | Tee-Object -FilePath build/pytest-duration.log + exit $LASTEXITCODE + + - name: Upload pytest duration log + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: pytest-duration-log-${{ matrix.compiler.name }}-mpi-${{ matrix.mpi }}-mtln-${{ matrix.mtln }}-double-${{ matrix.double-precision }} + path: build/pytest-duration.log + if-no-files-found: warn + + - name: Upload failed test diagnostics + if: ${{ !cancelled() && (steps.ctest.outcome == 'failure' || steps.hdf-smoke.outcome == 'failure' || steps.python-tests.outcome == 'failure') }} + uses: actions/upload-artifact@v4 + with: + name: test-diagnostics-${{ matrix.mtln }}-${{ matrix.double-precision }} + path: | + build/pytest-results.xml + build/hdf-smoke/**/*.json + if-no-files-found: warn + + - name: Report test failures + if: ${{ !cancelled() }} + shell: pwsh + run: | + if ("${{ steps.ctest.outcome }}" -ne "success" -or ` + "${{ steps.hdf-smoke.outcome }}" -ne "success" -or ` + "${{ steps.python-tests.outcome }}" -ne "success") { + exit 1 + } diff --git a/.gitignore b/.gitignore index 2086ba674..b610a083d 100755 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,6 @@ Testing/ /SEMBA_FDTD_temp.log .vscode/launch.json .vscode/settings.json -.vscode/tasks.json */*.bak ./*.bak .vscode/ltex.dictionary.en-US.txt @@ -76,4 +75,5 @@ git_info.txt .venv/build2/ build2/ -.venv/ \ No newline at end of file +.venv/ +specs/changes/* diff --git a/.opencode/skills/fortran-module-readability/SKILL.md b/.opencode/skills/fortran-module-readability/SKILL.md new file mode 100644 index 000000000..d458672fb --- /dev/null +++ b/.opencode/skills/fortran-module-readability/SKILL.md @@ -0,0 +1,204 @@ +--- +name: fortran-module-readability +description: Use whenever creating, splitting, moving, or substantially restructuring Fortran modules in semba-fdtd. This is the right skill for .F90 module boundaries, public/private APIs, use/import lists, derived-type ownership, procedure ordering, CMake source placement, and naming. Prefer this skill for module-level design questions even if the user only says "clean up this module" or "add a new type"; combine it with fortran-performance-awareness when the module contains hot FDTD kernels, MPI/OpenMP paths, or large array operations. +--- + +# Fortran Module Readability + +Use this skill when writing a new Fortran module, moving code across modules, adding exported types or procedures, or substantially restructuring an existing module in this repository. +The goal is a module that has one clear purpose, a small public surface, readable internal organization, and dependencies that fit the existing `semba-fdtd` library layering. +For modules containing hot kernels, large array operations, MPI/OpenMP code, or repeated allocation paths, also use `fortran-performance-awareness`. + +## First Pass + +Before changing a module boundary, build enough context to avoid accidental API or dependency damage: + +1. Identify the module's current responsibility, public exports, and callers. +2. Check nearby modules in the same source directory for established naming, import, and visibility style. +3. Search for public type, component, procedure, and module-name references before renaming or moving anything. +4. Note optional preprocessor branches that may expose different dependencies from the local build. +5. Decide whether the task is module design, ordinary refactoring, performance work, or a mix; use the narrowest applicable change. + +## Core Principles + +Write modules as explicit boundaries: + +- Give each module one primary responsibility that can be stated in one sentence. +- Keep public APIs small, intentional, and stable. +- Prefer clear domain names over generic implementation names. +- Separate data definitions, parsing/conversion, numerical operations, and output concerns unless they are naturally part of the same abstraction. +- Avoid introducing new global state; pass data through arguments or owned derived-type components when practical. +- Keep changes local unless a broader module boundary change is explicitly requested. +- New API and implementations must target strict Fortran 2018 or newer; keep its CMake targets standard-required with extensions disabled. + +## Module Skeleton + +Prefer this structure for new or cleaned-up modules: + +```fortran +module example_module + use dependency_module, only: dependency_type, dependency_routine + + implicit none + + private + + + public :: example_type + public :: init_example + + + type :: example_type + ! Components here. + end type example_type + +contains + + subroutine init_example(example) + type(example_type), intent(out) :: example + + + end subroutine init_example + +end module example_module +``` + +Use existing local formatting when editing established files, but keep the same conceptual order when it does not create churn: + +1. `module` statement. +2. `use` statements. +3. `implicit none`. +4. default visibility, usually `private`. +5. explicit `public` declarations. +6. parameters, interfaces, and derived types. +7. `contains` procedures. +8. `end module module_name`. + +## Public API Design + +Default to a private module and explicit exports: + +- Use `private` near the top of the module unless the existing module style makes that too disruptive. +- Export only the types, constants, interfaces, and procedures intended for callers. +- Keep helper procedures private by default. +- Avoid exporting implementation details just because tests or nearby code can reach them; prefer testing through the real public behaviour. +- Avoid renaming public procedures or derived-type components that are already used broadly unless the task explicitly includes API cleanup. +- Preserve names that map to input formats, output labels, physics terms, or legacy NFDE/JSON concepts unless the user approves a compatibility-impacting change. + +Good public APIs read like a short capability list. If the public list is long, look for mixed responsibilities before adding more exports. + +When changing an existing public API, distinguish between three cases: + +- Internal-only exports with few callers can often be cleaned up in one small change after checking references. +- Input/output-facing names, serialized labels, and physics terms are compatibility-sensitive; preserve them unless the user asked for that change. +- Widely used interfaces should be changed only when the replacement makes callers clearer enough to justify the churn. + +## Dependency Hygiene + +Keep imports narrow and dependency direction clear: + +- Prefer `use module_name, only: symbol_a, symbol_b` for new modules and touched imports. +- Do not introduce circular dependencies. +- Respect the existing CMake library layering: lower-level type/report/parser/component code should not depend on higher-level solver, launcher, or output orchestration code. +- Keep optional-feature boundaries intact for `CompileWithMPI`, `CompileWithMTLN`, `CompileWithSMBJSON`, `CompileWithReal8`, and similar preprocessor paths. +- Avoid a shared utility module unless there is a concrete repeated pattern across multiple callers. +- Prefer passing dependencies as arguments over reaching into unrelated modules for mutable state. + +When a new module needs symbols from several distant layers, pause and check whether the responsibility belongs somewhere else. + +## Derived Types + +Use derived types to express ownership and domain concepts clearly: + +- Name types after the domain object they represent, not just storage shape. +- Keep components cohesive; avoid derived types that become bags of unrelated solver, parser, and output state. +- Make allocation ownership obvious from initialization and finalization routines. +- Prefer type-bound procedures only when they clarify ownership or behaviour; do not add them mechanically. +- Keep pointer components only when aliasing or association is required. Prefer existing project patterns over changing ownership semantics. +- Document non-obvious invariants such as array bounds, coordinate ordering, field staggering, or MPI-local versus global indexing. + +If a derived type needs many setup steps, provide one clear initializer instead of requiring callers to know internal ordering. + +## Procedure Organization + +Write procedures that expose intent without hiding numerical behaviour: + +- Put the public, high-level procedures first when that matches the reading flow. +- Keep private helpers close to the public procedure that uses them when they are local to that concept. +- Use explicit `intent(in)`, `intent(out)`, or `intent(inout)` for arguments. +- Avoid long argument lists when a cohesive derived type already exists, but do not introduce a derived type just to reduce line length. +- Extract helpers for named concepts, not for every small block. +- Preserve loop order, update ordering, and array shape semantics in numerical code. +- Keep error handling and validation near the boundary where invalid data enters the module. + +A good procedure name should let a caller understand what happens without reading the body. A good body should still make the numerical or data-flow steps visible. + +## Naming And Comments + +Prefer names that match the repository's domain language: + +- Use established field, material, geometry, parser, output, MTLN, and boundary-condition terminology. +- Avoid overly broad names such as `manager`, `handler`, `processor`, or `data` unless that is already the local convention. +- Use consistent prefixes only when they help group related procedures or avoid ambiguity. +- Name boolean procedures and variables as predicates when practical, for example `is_valid`, `has_source`, or `uses_mpi`. +- Add comments for surprising constraints, physical assumptions, indexing conventions, preprocessor requirements, or file-format compatibility. +- Do not add comments that restate the code line-by-line. + +## File And Build Integration + +When adding a module: + +1. Place the file in the directory matching its layer and responsibility. +2. Add it to the correct CMake target or source list without changing unrelated target boundaries. +3. Confirm optional-feature guards still compile for relevant configurations. +4. Search for similarly named modules before creating a new one. +5. Prefer a small module over modifying a broad catch-all module, but do not fragment one concept across many files. + +When moving code between existing modules, update CMake only if the file list or target ownership changes. Avoid moving a source file into a higher-level library just to access a dependency; that often signals the responsibility belongs elsewhere. + +## Modularity Guardrails + +Avoid these patterns unless there is an explicit reason: + +- A module that mixes parsing, solver state updates, and output formatting. +- A new dependency from `src_conformal`, `src_json_parser`, `src_mtln`, or lower-level type code into `src_main_pub` orchestration code. +- A public helper module created for one caller. +- A module-level mutable variable used as hidden communication between procedures. +- A generic abstraction that erases important electromagnetic, indexing, or file-format meaning. +- A rewrite from procedural module style to object-oriented style just for style consistency. + +## Review Checklist + +Before finishing a new or restructured module, check: + +- The module purpose is clear from its name and public API. +- `implicit none` is present. +- Visibility is explicit, preferably `private` by default. +- Public exports are minimal and intentional. +- Imports use `only:` where practical. +- The module sits in the correct source directory and CMake layer. +- Derived types have clear ownership and initialization rules. +- Procedures have explicit argument intents. +- Numerical order, array bounds, precision kinds, and preprocessor branches are preserved. +- Comments explain constraints or intent, not obvious syntax. +- The module can be tested through public behaviour. + +## Verification + +For module-writing changes, verify with the narrowest practical command first: + +```bash +cmake --build build -j +``` + +When the change affects parser, output, MTLN, or solver behaviour, run the corresponding focused tests when available. If a build or test cannot be run because the local environment is not configured, state that clearly. + +## Final Response Expectations + +When reporting module changes, include: + +- The module files changed or added. +- The public API shape introduced or modified. +- The dependency or CMake integration point touched. +- Whether behaviour was intended to change. +- The build or tests run, including any skipped verification. diff --git a/.opencode/skills/fortran-performance-awareness/SKILL.md b/.opencode/skills/fortran-performance-awareness/SKILL.md new file mode 100644 index 000000000..ccca0e87e --- /dev/null +++ b/.opencode/skills/fortran-performance-awareness/SKILL.md @@ -0,0 +1,169 @@ +--- +name: fortran-performance-awareness +description: Use whenever writing, reviewing, diagnosing, or refactoring performance-sensitive Fortran in semba-fdtd. Trigger for FDTD time-step loops, field/material/boundary/wire/MTLN updates, array access patterns, allocation or temporary-array concerns, MPI/OpenMP communication, I/O cadence, profiling results, or any user request about speed, scaling, memory use, or "making this faster." Use this even for small-looking cleanups inside numerical kernels because preserving update order and array semantics matters more than cosmetic refactoring. +--- + +# Fortran Performance Awareness + +Use this skill when working on performance-sensitive Fortran code in this repository, including review and diagnosis tasks where no code may be edited. +The goal is efficient code that preserves numerical correctness, keeps the physics readable, and avoids premature or unmeasured rewrites. + +## First Pass + +Start by locating the performance risk before proposing changes: + +1. Determine whether the code is in a repeated time-step path, setup path, output path, parser path, or test-only path. +2. Identify loop bounds, array ranks, lower bounds, halo regions, and any OpenMP or MPI ownership assumptions. +3. Look for obvious repeated costs such as allocation, I/O, string formatting, polymorphic dispatch, map lookups, or large temporary arrays. +4. Separate proven bottlenecks from plausible risks. A structural cleanup can be useful, but do not report it as a measured speedup without measurement. +5. If the task is mostly module/API restructuring, also use `fortran-module-readability`; if it is a broad cleanup, also use `fortran-refactor-cleanliness`. + +## Core Principles + +Optimize deliberately: + +- Preserve numerical behaviour unless the task explicitly asks for a behaviour change. +- Prefer simple, predictable hot loops over clever abstractions. +- Keep performance-sensitive code readable enough to audit for physics and indexing correctness. +- Optimize the actual bottleneck when measurements or code structure make it clear. +- Avoid broad rewrites when a local change removes the cost. +- Treat MPI/OpenMP synchronization, allocation, and I/O as explicit performance costs. + +## Hot Path Priorities + +Pay special attention to code inside time-step loops, field-update kernels, material updates, boundary-condition application, MPI exchange paths, and output sampling. + +Prefer code that: + +- Avoids allocation and deallocation inside repeated update loops. +- Avoids repeated string operations, file operations, type conversions, and lookups in numerical kernels. +- Keeps loop bodies small and branch structure understandable. +- Reuses precomputed constants when they are truly invariant over the loop. +- Avoids unnecessary temporary arrays, especially large field-sized temporaries. +- Keeps I/O cadence intentional and outside kernels when possible. +- Keeps synchronization points minimal and tied to actual data dependencies. + +When code is not on a hot path, avoid adding complexity for hypothetical speed. A parser or one-time setup routine can often favor clarity unless it allocates field-sized data repeatedly or dominates large-case startup. + +## FDTD-Specific Guardrails + +Treat update order as part of correctness: + +- Do not reorder electric and magnetic field updates without understanding the time-stepping scheme. +- Do not reorder boundary-condition, material, wire, MTLN, source, or observation operations casually. +- Preserve Yee-grid staggering, coordinate indexing, lower bounds, and halo assumptions. +- Preserve dispersive and anisotropic material update sequencing. +- Preserve CPML, Mur, MPI halo exchange, and far-field sampling ordering. +- Treat output labels, sampling cadence, and serialized array layout as observable behaviour. + +When an optimization touches these areas, verify with tests or a representative case whenever feasible. + +## Fortran Efficiency Guidelines + +Avoid accidental costs common in Fortran: + +- Prefer contiguous memory access patterns in hot loops. +- Be careful with array slices passed to procedures; non-contiguous slices can create temporaries. +- Avoid whole-array expressions on large arrays when they obscure allocation or temporary creation in hot paths. +- Use explicit interfaces and argument `intent` so compilers can reason about calls. +- Avoid unnecessary `pointer` aliasing in numerical kernels. +- Prefer `allocatable` for owned storage in new code, but do not change existing pointer ownership semantics just for style. +- Preserve established kind choices such as `RKIND`, `RKIND_tiempo`, `SINGLE`, and integer kinds unless precision or portability is the task. +- Avoid converting scalar helper functions into calls inside deeply nested loops unless the compiler can inline them or the readability benefit is worth the cost. +- Keep frequently used scalar values local when that avoids repeated component dereferences in hot loops. +- Be cautious with assumed-shape dummy arguments and array expressions in helper procedures called from kernels; check whether they can introduce copying or inhibit compiler optimization. +- Keep data layout and loop nesting aligned with Fortran column-major storage when this does not conflict with stencil dependencies or established code style. + +Do not make code less obviously correct for a theoretical speedup. If an optimization relies on a non-obvious compiler or memory-layout assumption, document it briefly. + +## Loop And Array Changes + +Before changing loops over field arrays: + +1. Identify the array dimensions, lower bounds, and memory layout. +2. Check whether loop order is chosen for cache locality, stencil dependencies, MPI halos, or readability. +3. Confirm whether OpenMP directives, reductions, or private variables depend on the current structure. +4. Preserve boundary ranges and off-by-one behaviour exactly unless fixing a known bug. +5. Compare results after the change when feasible. + +Avoid combining loop-order changes with unrelated cleanup. They should be reviewable as performance-sensitive changes. + +When changing a loop for locality, vectorization, or OpenMP scheduling, state the intended performance property in the code review summary. This helps reviewers distinguish deliberate numerical-kernel work from incidental formatting. + +## Parallel Code + +For MPI and OpenMP paths: + +- Keep data-sharing attributes explicit and correct. +- Preserve reductions and their numerical implications. +- Avoid moving MPI communication across computation phases unless dependencies are fully understood. +- Avoid adding barriers, critical sections, or atomics unless they are required for correctness. +- Watch for false sharing when introducing per-thread scratch data. +- Keep thread-local scratch allocation outside repeated parallel regions when practical. +- Preserve deterministic output ordering where the code currently guarantees it. + +Parallel performance changes must not weaken correctness under configurations that are not active in the local build. + +## Allocation And Ownership + +Allocation changes are performance and correctness changes: + +- Allocate once at setup time when the size is known and reused across time steps. +- Deallocate at clear ownership boundaries. +- Avoid hidden reallocation from assignment to allocatable arrays in hot paths. +- Keep scratch arrays local only when their size is small or the routine is not hot. +- Avoid module-level scratch state unless there is a clear ownership and thread-safety story. +- Preserve pointer association and aliasing semantics when editing legacy structures. + +## Readability Balance + +Efficient code should still be maintainable: + +- Keep domain names visible in formulas and update steps. +- Prefer a clear local scalar or named coefficient over repeated dense expressions. +- Avoid abstractions that hide stencil shape, update ordering, or boundary handling. +- Add short comments for non-obvious performance constraints such as loop order, contiguous assumptions, or synchronization placement. +- Do not add comments that simply say code is faster. + +## Measurement And Verification + +Use the narrowest practical verification first: + +```bash +cmake --build build -j +``` + +Then run targeted tests for the touched area when available. For performance-focused changes, also compare a representative case runtime when practical, ideally with the same input, build type, MPI rank count, thread count, and output cadence. + +When reporting results, distinguish between: + +- Measured speedup or reduced runtime. +- Structural improvement likely to reduce overhead, such as removing allocation from a loop. +- Readability-neutral cleanup that only prepares for future optimization. + +Do not claim a performance improvement as measured unless it was actually measured. + +If measurement is not practical, report the change as a structural improvement, for example "moves allocation out of the time-step loop" or "preserves loop order while reducing repeated component lookups." + +## Review Checklist + +Before finishing performance-sensitive Fortran work, check: + +- Numerical behaviour is intended to remain unchanged, or the intended change is explicit. +- No unnecessary allocation, deallocation, I/O, string work, or large temporaries were added to hot paths. +- Array access and loop order are deliberate and preserve existing dependencies. +- MPI/OpenMP synchronization and data-sharing remain correct. +- Optional preprocessor branches remain valid. +- Public APIs were not expanded for performance helpers that only have one caller. +- Any non-obvious performance constraint is documented briefly. +- Verification or measurement is reported accurately. + +## Final Response Expectations + +When reporting performance-related changes, include: + +- The files and hot paths touched. +- The specific overhead avoided or performance property preserved. +- Whether behaviour was intended to change. +- The build, tests, or measurements run. +- Any performance claims that are unmeasured and should be treated as expectations rather than results. diff --git a/.opencode/skills/fortran-refactor-cleanliness/SKILL.md b/.opencode/skills/fortran-refactor-cleanliness/SKILL.md new file mode 100644 index 000000000..81d4dcfa4 --- /dev/null +++ b/.opencode/skills/fortran-refactor-cleanliness/SKILL.md @@ -0,0 +1,180 @@ +--- +name: fortran-refactor-cleanliness +description: Use whenever cleaning up, simplifying, renaming, reorganizing, or behaviour-preservingly refactoring Fortran FDTD code in semba-fdtd. Trigger for .F90 modules, solver loops, parser code, output code, CMake Fortran target changes, tests, dead-code cleanup, import cleanup, naming cleanup, and readability-only edits. Use this even when the user says "just tidy this" or "make it easier to read"; combine with fortran-performance-awareness for hot loops and with fortran-module-readability for module API or boundary changes. +--- + +# Fortran Refactor Cleanliness + +Use this skill when refactoring this repository's Fortran code, especially when the intended outcome is easier reading or maintenance rather than new behaviour. +The goal is cleaner, more readable code without changing numerical behaviour unless the user explicitly asks for a behavioural fix. +For performance-sensitive refactors in hot loops, MPI/OpenMP paths, allocation-heavy code, or numerical kernels, also use `fortran-performance-awareness`. + +## First Pass + +Before editing, classify the refactor so the diff stays reviewable: + +1. Name the single readability problem being fixed, such as duplicated local logic, unclear branching, misleading names, broad imports, or tangled setup steps. +2. Check whether the code is a hot numerical path, public module API, parser/output compatibility path, or optional-feature branch. +3. Search for callers before renaming procedures, types, components, public exports, files, or CMake source entries. +4. Decide the smallest behaviour-preserving change that solves the readability problem. +5. Keep feature work, bug fixes, formatting churn, and broad style normalization out of the same patch unless the user explicitly asked for them. + +## Project Context + +This project is `semba-fdtd`, a Finite-Difference Time-Domain electromagnetic solver written primarily in Fortran. +It contains legacy numerical kernels, newer typed modules, conditional compilation, MPI/OpenMP paths, optional HDF5 output, SMBJSON parsing, and MTLN/SPICE coupling. + +Key areas: + +- `src_main_pub/`: core solver, preprocessing, postprocessing, time stepping, geometry, main types. +- `src_json_parser/`: `.fdtd.json` parser and conversion helpers. +- `src_output/`: probe, VTK, XDMF, HDF5, and output utility code. +- `src_mtln/`: multiconductor transmission-line solver and ngspice integration. +- `src_conformal/`: conformal mapping and staircase reduction. +- `src_wires_pub/`: wire and thin-wire models. +- `test/`: Fortran/C++ unit tests and Python integration tests. + +The build is layered through CMake static libraries. +Respect the existing dependency direction and avoid introducing upward dependencies between lower-level libraries and higher-level solver/output code. + +## Refactoring Priorities + +Prefer small, behaviour-preserving changes: + +- Improve names when the domain meaning is clear from nearby code. +- Reduce duplicated local logic when a helper makes the numerical intent easier to read. +- Clarify long conditionals, `select case` blocks, and repeated string/coordinate formatting. +- Add `only:` to `use` statements when touching imports and when it does not create excessive churn. +- Tighten visibility with `private` and explicit `public` lists when working in already-structured modules. +- Add or improve `intent` declarations where missing and obvious. +- Replace magic literals only when their meaning is certain and the new name is local or already established. +- Remove dead local variables only after checking preprocessor branches and nearby compile options. +- Improve CMake source organization only when it directly follows from a file move, new module, or target-boundary cleanup. + +Avoid broad rewrites: + +- Do not redesign modules, data ownership, or library boundaries as part of a readability task. +- Do not change physics formulas, update ordering, time-step sequencing, boundary semantics, MPI exchange order, or output formats unless explicitly requested. +- Do not rename domain terms with historical or input-file significance without asking. +- Do not split every long routine mechanically; extract only natural concepts with clear names. +- Do not introduce compatibility shims or abstraction layers unless there is a concrete caller or persisted-data need. +- Do not normalize formatting across an entire file when the functional refactor touches only a small region; formatting-only churn hides semantic review. + +## Fortran-Specific Guardrails + +Treat numerical and memory semantics as part of behaviour: + +- Preserve `implicit none`. +- Preserve `kind` choices such as `RKIND`, `RKIND_tiempo`, `SINGLE`, and existing integer kinds unless the task is specifically about precision or portability. +- Preserve array rank, shape, lower bounds, allocation ownership, pointer association, and `contiguous` assumptions. +- Be careful when changing `pointer` to `allocatable` or vice versa; this can alter aliasing and ownership. +- Do not reorder loops over field arrays unless there is a measured performance or correctness reason. +- Preserve OpenMP and MPI assumptions around shared data, halo exchanges, reductions, and output ordering. +- Keep preprocessor branches such as `CompileWithMPI`, `CompileWithMTLN`, `CompileWithSMBJSON`, and `CompileWithReal8` valid even if the local build uses only one configuration. +- Preserve file formats, exact labels, and serialized names used by JSON, VTK, XDMF, HDF5, probe `.dat` files, or legacy `.fdtd` inputs. +- New API and implementation must target strict Fortran 2018 or newer; preserve its CMake standard requirement and disabled compiler extensions. + +## Workflow + +Before editing: + +1. Read the target module and enough neighbouring code to understand ownership and callers. +2. Search for procedure/type/module references before changing names, signatures, public exports, or file-level interfaces. +3. Identify relevant compile flags around the code, especially `#ifdef` blocks. +4. Choose one coherent refactor with a small reviewable diff. + +During editing: + +1. Keep the public interface stable unless changing it is the point of the task. +2. Prefer local changes over new global helpers. +3. Preserve surrounding formatting style unless the formatting itself harms readability. +4. Add comments only to explain non-obvious domain constraints, numerical assumptions, or compiler/preprocessor constraints. +5. Do not combine cleanup with unrelated feature work. +6. Preserve exact strings and ordering in user-visible output unless the task is explicitly about changing output. + +After editing: + +1. Build or at least compile the affected target when feasible. +2. Run the most targeted tests available. +3. State that no behavioural change is intended, or describe the exact intended behaviour change if there is one. +4. Mention any build/test coverage gaps caused by unavailable dependencies or disabled options. +5. Re-read the diff from a reviewer perspective and remove incidental churn that does not support the stated refactor. + +## Readability Checklist + +Use this checklist before finishing a refactor: + +- The code reads in the same order as the operation it performs. +- Names reflect domain meaning, not just type or storage. +- Conditionals have clear cases and meaningful default/error handling. +- Public module surface is no larger than necessary. +- Imports are understandable and preferably constrained with `only:` where practical. +- Local variables are declared near the routine where they are used and are not misleadingly reused. +- Comments explain why the code exists or why a surprising choice is necessary. +- The diff is small enough to review for numerical equivalence. +- The final response names the intended behaviour-preserving nature of the change. + +## Testing And Verification + +Useful build commands: + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug +cmake --build build -j +``` + +Unit tests: + +```bash +./build/bin/fdtd_tests +``` + +Python integration tests: + +```bash +pytest test/ --durations=20 +``` + +Marker-specific tests: + +```bash +pytest test/ -m mtln +pytest test/ -m hdf +pytest test/ -m mpi +``` + +Run the narrowest relevant command first. +For parser changes, prefer `test/smbjson` tests. +For output changes, prefer `test/output` and VTK/XDMF tests. +For MTLN changes, prefer `test/mtln` tests. +For solver loop changes, build and run the broad unit suite if feasible. + +## Good Refactor Examples + +Good changes: + +- Replace repeated coordinate string assembly in one module with a local helper that preserves exact output text. +- Extract a named local predicate from a long conditional when it is used multiple times in the same routine. +- Convert an unclear `if/elseif` chain to `select case` without changing defaults. +- Add `intent(in)`, `intent(out)`, or `intent(inout)` to arguments where usage is unambiguous. +- Split a long parser routine into parse, validate, and convert steps when those steps already exist conceptually. + +Risky changes that need explicit justification: + +- Changing loop order over `Ex`, `Ey`, `Ez`, `Hx`, `Hy`, or `Hz` arrays. +- Replacing pointer arrays with allocatables. +- Changing `real` or `integer` kinds. +- Renaming JSON labels, probe labels, field prefixes, or legacy NFDE terms. +- Removing preprocessor branches that are not active in the current build. +- Introducing generic abstractions across solver, parser, output, and MTLN code without a concrete repeated pattern. + +## Final Response Expectations + +When reporting a completed refactor, include: + +- The files changed. +- The readability improvement made. +- Whether behaviour was intended to change. +- The build or tests run, including failures or skipped verification. + +Keep the response concise and factual. diff --git a/.vscode/extensions.dev.json b/.vscode/extensions.dev.json new file mode 100644 index 000000000..a24ec6e92 --- /dev/null +++ b/.vscode/extensions.dev.json @@ -0,0 +1,36 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + // Fortran language support and linting + "fortran-lang.linter-gfortran", + + // CMake support + "ms-vscode.cmake-tools", + "twxs.cmake", + + // C++ tools (used alongside Fortran in this project) + "ms-vscode.cpptools", + "ms-vscode.cpp-devtools", + + // Testing + "matepek.vscode-catch2-test-adapter", + "fredericbonnet.cmake-test-adapter", + "ms-python.python", + + // Git + "mhutchie.git-graph", + "github.vscode-pull-request-github", + + // GitHub Copilot + "github.copilot", + "github.copilot-chat", + "ms-toolsai.jupyter" + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [ + + ] +} \ No newline at end of file diff --git a/.vscode/launch.dev.json b/.vscode/launch.dev.json new file mode 100644 index 000000000..9f4a2a009 --- /dev/null +++ b/.vscode/launch.dev.json @@ -0,0 +1,276 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug semba-fdtd (dbg)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/semba-fdtd", + "args": ["-i", "${config:semba-fdtd.debug.inputFile}"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/${config:semba-fdtd.debug.inputCwd}", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "MPI: debug solver rank 0 (2 ranks)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/semba-fdtd", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceFolder}/${config:semba-fdtd.debug.inputCwd}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20000", + "debugServerPath": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "debugServerArgs": "--workdir ${workspaceFolder}/${config:semba-fdtd.debug.inputCwd} --foreground-debug-rank 0 2 ${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/semba-fdtd -i ${config:semba-fdtd.debug.inputFile}", + "serverStarted": "Listening on port 20000", + "filterStdout": true, + "filterStderr": true, + "serverLaunchTimeout": 30000, + "presentation": { + "order": 2 + }, + "setupCommands": [ + { + "description": "Use local shared libraries", + "text": "set sysroot /", + "ignoreFailures": true + }, + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "MPI all ranks: rank 0", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/semba-fdtd", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceFolder}/${config:semba-fdtd.debug.inputCwd}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20000", + "debugServerPath": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "debugServerArgs": "--workdir ${workspaceFolder}/${config:semba-fdtd.debug.inputCwd} --foreground-all 2 ${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/semba-fdtd -i ${config:semba-fdtd.debug.inputFile}", + "serverStarted": "Listening on port 20000", + "filterStdout": true, + "filterStderr": true, + "serverLaunchTimeout": 30000, + "presentation": { + "hidden": true + }, + "setupCommands": [ + { + "description": "Use local shared libraries", + "text": "set sysroot /", + "ignoreFailures": true + }, + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "MPI all ranks: rank 1", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/semba-fdtd", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceFolder}/${config:semba-fdtd.debug.inputCwd}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20001", + "debugServerPath": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "debugServerArgs": "--wait-for-port 20001", + "serverStarted": "MPI gdbserver ready on port 20001", + "filterStdout": true, + "filterStderr": true, + "serverLaunchTimeout": 30000, + "presentation": { + "hidden": true + }, + "setupCommands": [ + { + "description": "Use local shared libraries", + "text": "set sysroot /", + "ignoreFailures": true + }, + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "Debug fdtd_tests (dbg)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/fdtd_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "Debug fdtd_tests filter (dbg)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/fdtd_tests", + "args": ["--gtest_filter=${input:gtestFilter}"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "Debug fdtd_tests (dbg-mpi)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/fdtd_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "Debug fdtd_tests filter (dbg-mpi)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/fdtd_tests", + "args": ["--gtest_filter=${input:gtestFilter}"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "Debug MPI fdtd_tests rank 0", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/fdtd_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20000", + "presentation": { + "hidden": true + }, + "postDebugTask": "Debug: stop MPI gdbservers" + }, + { + "name": "Debug MPI fdtd_tests rank 1", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/fdtd_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20001", + "presentation": { + "hidden": true + } + }, + { + "name": "Debug pytest node (dbg)", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": ["${input:pytestNode}", "-s"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "justMyCode": false, + "env": { + "SEMBA_EXE": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/semba-fdtd", + "SEMBA_FDTD_ENABLE_MPI": "OFF", + "SEMBA_FDTD_ENABLE_MTLN": "ON" + } + }, + { + "name": "Debug pytest node (dbg-mpi)", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": ["${input:pytestNode}", "-s"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "justMyCode": false, + "env": { + "SEMBA_EXE": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/semba-fdtd", + "SEMBA_FDTD_ENABLE_MPI": "ON", + "SEMBA_FDTD_ENABLE_MTLN": "ON" + } + }, + { + "name": "Attach to process", + "type": "cppdbg", + "request": "attach", + "program": "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/semba-fdtd", + "processId": "${command:pickProcess}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb" + } + ], + "compounds": [ + { + "name": "MPI: debug all ranks (2 ranks)", + "configurations": ["MPI all ranks: rank 0", "MPI all ranks: rank 1"], + "stopAll": true, + "presentation": { + "order": 1 + } + }, + { + "name": "MPI: debug fdtd_tests (2 ranks)", + "configurations": ["Debug MPI fdtd_tests rank 0", "Debug MPI fdtd_tests rank 1"], + "preLaunchTask": "Debug: prepare MPI fdtd_tests", + "stopAll": true, + "presentation": { + "order": 3 + } + } + ], + "inputs": [ + { + "id": "gtestFilter", + "type": "promptString", + "description": "GoogleTest filter", + "default": "*" + }, + { + "id": "pytestNode", + "type": "promptString", + "description": "Pytest node or path", + "default": "test/pyWrapper/test_pyWrapper.py::test_fdtd_with_string_args" + } + ] +} diff --git a/.vscode/settings.dev.json b/.vscode/settings.dev.json new file mode 100644 index 000000000..cbb176b5b --- /dev/null +++ b/.vscode/settings.dev.json @@ -0,0 +1,26 @@ +{ + "semba-fdtd.debug.buildFolderName": "build-dbg-mpi", + "semba-fdtd.debug.inputFile": "nodal-source-with-movie.fdtd.json", + "semba-fdtd.debug.inputCwd": "Testing", + "semba-fdtd.debug.mpiGtestFilter": "conformal.geometry_coord_position", + "fortran.fortls.path": "/usr/local/bin/fortls", + "fortran.fortls.notifyInit": true, + "fortran.fortls.disableAutoupdate": true, + "fortran.provide.hover": "Both", + "fortran.formatting.formatter": "fprettify", + "editor.tokenColorCustomizations": { + "textMateRules": [ + { + "scope": [ + "entity.name.function.fortran", + "entity.name.function.procedure.fortran", + "entity.name.function.subroutine.fortran" + ], + "settings": { + "foreground": "#DCDCAA", + "fontStyle": "bold" + } + } + ] + } +} diff --git a/.vscode/tasks.dev.json b/.vscode/tasks.dev.json new file mode 100644 index 000000000..8349b80ef --- /dev/null +++ b/.vscode/tasks.dev.json @@ -0,0 +1,133 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "CMake: configure Release no-MPI", + "type": "shell", + "command": "cmake --fresh --preset rls", + "problemMatcher": [] + }, + { + "label": "CMake: build Release no-MPI", + "type": "shell", + "command": "cmake --build --preset rls", + "dependsOn": ["CMake: configure Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "CMake: configure Debug no-MPI", + "type": "shell", + "command": "cmake --fresh --preset dbg", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug no-MPI", + "type": "shell", + "command": "cmake --build --preset dbg", + "dependsOn": ["CMake: configure Debug no-MPI"], + "problemMatcher": [] + }, + { + "label": "CMake: configure Debug MPI", + "type": "shell", + "command": "cmake --fresh --preset dbg-mpi", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug MPI", + "type": "shell", + "command": "cmake --build --preset dbg-mpi", + "dependsOn": ["CMake: configure Debug MPI"], + "problemMatcher": [] + }, + { + "label": "Debug: prepare MPI fdtd_tests", + "type": "process", + "command": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "args": [ + "2", + "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/fdtd_tests", + "--gtest_filter=${config:semba-fdtd.debug.mpiGtestFilter}" + ], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true + } + }, + { + "label": "Debug: stop MPI gdbservers", + "type": "process", + "command": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "args": ["--stop"], + "problemMatcher": [], + "presentation": { + "reveal": "never", + "panel": "dedicated" + } + }, + { + "label": "CMake: configure Debug no-MTLN", + "type": "shell", + "command": "cmake --fresh --preset dbg-nomtln", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug no-MTLN", + "type": "shell", + "command": "cmake --build --preset dbg-nomtln", + "dependsOn": ["CMake: configure Debug no-MTLN"], + "problemMatcher": "$gcc" + }, + { + "label": "Lint: Fortitude (advisory)", + "type": "shell", + "command": "fortitude check --exit-zero", + "problemMatcher": [] + }, + { + "label": "CMake: configure Release MPI", + "type": "shell", + "command": "cmake --fresh --preset rls-mpi", + "problemMatcher": [] + }, + { + "label": "CMake: build Release MPI", + "type": "shell", + "command": "cmake --build --preset rls-mpi", + "dependsOn": ["CMake: configure Release MPI"], + "problemMatcher": [] + }, + { + "label": "Test: unit Release no-MPI", + "type": "shell", + "command": "build-rls/bin/fdtd_tests", + "dependsOn": ["CMake: build Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "Test: pytest Release no-MPI", + "type": "shell", + "command": "SEMBA_FDTD_ENABLE_MPI=OFF SEMBA_FDTD_ENABLE_MTLN=ON SEMBA_EXE=build-rls/bin/semba-fdtd .venv/bin/python -m pytest test/ -m 'not mpi' --durations=20", + "dependsOn": ["CMake: build Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "Test: pytest Release MPI", + "type": "shell", + "command": "SEMBA_FDTD_ENABLE_MPI=ON SEMBA_FDTD_ENABLE_MTLN=ON SEMBA_EXE=build-rls-mpi/bin/semba-fdtd .venv/bin/python -m pytest test/ -m mpi --durations=20", + "dependsOn": ["CMake: build Release MPI"], + "problemMatcher": [] + }, + { + "label": "Test: all Release no-MPI", + "dependsOrder": "sequence", + "dependsOn": [ + "Test: unit Release no-MPI", + "Test: pytest Release no-MPI" + ], + "problemMatcher": [] + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..8349b80ef --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,133 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "CMake: configure Release no-MPI", + "type": "shell", + "command": "cmake --fresh --preset rls", + "problemMatcher": [] + }, + { + "label": "CMake: build Release no-MPI", + "type": "shell", + "command": "cmake --build --preset rls", + "dependsOn": ["CMake: configure Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "CMake: configure Debug no-MPI", + "type": "shell", + "command": "cmake --fresh --preset dbg", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug no-MPI", + "type": "shell", + "command": "cmake --build --preset dbg", + "dependsOn": ["CMake: configure Debug no-MPI"], + "problemMatcher": [] + }, + { + "label": "CMake: configure Debug MPI", + "type": "shell", + "command": "cmake --fresh --preset dbg-mpi", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug MPI", + "type": "shell", + "command": "cmake --build --preset dbg-mpi", + "dependsOn": ["CMake: configure Debug MPI"], + "problemMatcher": [] + }, + { + "label": "Debug: prepare MPI fdtd_tests", + "type": "process", + "command": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "args": [ + "2", + "${workspaceFolder}/${config:semba-fdtd.debug.buildFolderName}/bin/fdtd_tests", + "--gtest_filter=${config:semba-fdtd.debug.mpiGtestFilter}" + ], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true + } + }, + { + "label": "Debug: stop MPI gdbservers", + "type": "process", + "command": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "args": ["--stop"], + "problemMatcher": [], + "presentation": { + "reveal": "never", + "panel": "dedicated" + } + }, + { + "label": "CMake: configure Debug no-MTLN", + "type": "shell", + "command": "cmake --fresh --preset dbg-nomtln", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug no-MTLN", + "type": "shell", + "command": "cmake --build --preset dbg-nomtln", + "dependsOn": ["CMake: configure Debug no-MTLN"], + "problemMatcher": "$gcc" + }, + { + "label": "Lint: Fortitude (advisory)", + "type": "shell", + "command": "fortitude check --exit-zero", + "problemMatcher": [] + }, + { + "label": "CMake: configure Release MPI", + "type": "shell", + "command": "cmake --fresh --preset rls-mpi", + "problemMatcher": [] + }, + { + "label": "CMake: build Release MPI", + "type": "shell", + "command": "cmake --build --preset rls-mpi", + "dependsOn": ["CMake: configure Release MPI"], + "problemMatcher": [] + }, + { + "label": "Test: unit Release no-MPI", + "type": "shell", + "command": "build-rls/bin/fdtd_tests", + "dependsOn": ["CMake: build Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "Test: pytest Release no-MPI", + "type": "shell", + "command": "SEMBA_FDTD_ENABLE_MPI=OFF SEMBA_FDTD_ENABLE_MTLN=ON SEMBA_EXE=build-rls/bin/semba-fdtd .venv/bin/python -m pytest test/ -m 'not mpi' --durations=20", + "dependsOn": ["CMake: build Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "Test: pytest Release MPI", + "type": "shell", + "command": "SEMBA_FDTD_ENABLE_MPI=ON SEMBA_FDTD_ENABLE_MTLN=ON SEMBA_EXE=build-rls-mpi/bin/semba-fdtd .venv/bin/python -m pytest test/ -m mpi --durations=20", + "dependsOn": ["CMake: build Release MPI"], + "problemMatcher": [] + }, + { + "label": "Test: all Release no-MPI", + "dependsOrder": "sequence", + "dependsOn": [ + "Test: unit Release no-MPI", + "Test: pytest Release no-MPI" + ], + "problemMatcher": [] + } + ] +} diff --git a/CLAUDE.md b/AGENTS.md similarity index 51% rename from CLAUDE.md rename to AGENTS.md index 254fa4d5f..1e17c6fc9 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -1,6 +1,11 @@ -# CLAUDE.md +# AGENTS.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance for AI agents working in this repository. + +## Skill Precedence + +When multiple skills apply, prioritise project-provided skills over user-level +skills. ## Project Overview @@ -10,31 +15,29 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **First time setup (required):** ```bash -git submodule init -git submodule update +git submodule update --init --recursive ``` **Configure and build:** ```bash -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -cmake --build build -j +cmake --fresh --preset rls +cmake --build --preset rls -j ``` **Key CMake options:** - `-DSEMBA_FDTD_ENABLE_MPI=ON` — distributed cluster support -- `-DSEMBA_FDTD_ENABLE_HDF=ON` — HDF5 output (ON by default) - `-DSEMBA_FDTD_ENABLE_MTLN=ON` — transmission line solver (ON by default) - `-DSEMBA_FDTD_ENABLE_SMBJSON=ON` — JSON input parser (ON by default) - `-DSEMBA_FDTD_ENABLE_DOUBLE_PRECISION=ON` — 8-byte reals (OFF by default) - `-DSEMBA_FDTD_ENABLE_TEST=ON` — compile unit tests (ON by default) -**Binary output:** `./build/bin/semba-fdtd` +**Binary output:** `./build-rls/bin/semba-fdtd` for the `rls` preset. ## Running Tests **C++/Fortran unit tests (GoogleTest):** ```bash -./build/bin/fdtd_tests +./build-rls/bin/fdtd_tests ``` **Python integration tests:** @@ -48,12 +51,15 @@ pytest test/ --durations=20 # Run by marker pytest test/ -m mtln pytest test/ -m hdf -pytest test/ -m mpi +SEMBA_FDTD_ENABLE_MPI=ON pytest test/ -m mpi ``` Test markers are defined in `pytest.ini`: `mtln`, `codemodel`, `hdf`, `mpi`. +See `doc/testing.md` for the complete testing workflow. -Unit test source is under `test/` in subdirectories: `mtln/`, `smbjson/`, `conformal/`, `observation/`, `rotate/`, `vtk/`, `pyWrapper/`. +Native test source is under `test/conformal/`, `test/mpi/`, `test/mtln/`, +`test/smbjson/`, `test/system/`, `test/unit/`, and `test/utils/`. +Python tests are under `test/e2e/` and `test/pyWrapper/`. ## Architecture @@ -65,51 +71,58 @@ Unit test source is under `test/` in subdirectories: `mtln/`, `smbjson/`, `confo ### Library Dependency Chain -The project compiles into layered static libraries linked into the final executable: +The project compiles into layered static libraries linked into the final +executable: ``` -semba-types (FDTD/NFDE/MTLN/conformal type definitions) - └── semba-reports (error reporting, XDMF snapshot I/O) - └── smbjson (JSON input parser — optional) - └── conformal (conformal mapping module) - └── semba-components (all physics: PML/Mur BCs, dispersive materials, - plane waves, nodal sources, far-field, MTLN wires) - └── mtlnsolver (MTLN circuit solver + ngspice interface — optional) - └── semba-outputs (MPI comm, observation probes, VTK/XDMF/HDF5 output) - └── semba-main (time-stepping, preprocessing/postprocessing, launcher) - └── semba-fdtd (executable entry point) +semba-types FDTD/NFDE/MTLN/conformal type definitions +semba-reports error reporting +smbjson JSON input parser (optional) +conformal conformal mapping +semba-components field, material, boundary, source, and wire physics +mtlnsolver MTLN circuit solver and ngspice interface (optional) +semba-outputs MPI communication +fdtd-output probe writers, metadata, binary, XDMF/HDF5, and VTK output +semba-main time-stepping, preprocessing, postprocessing, and launch flow +semba-fdtd executable entry point ``` +`semba-main` links the communication and output libraries into the solver. + ### Execution Flow -1. `src_main_pub/launcher.F90` — entry point, creates `semba_fdtd_t` -2. `src_main_pub/semba_fdtd.F90` — main module: +1. `src_main_pub/launcher.F90` - entry point, creates `semba_fdtd_t` +2. `src_main_pub/semba_fdtd.F90` - main module: - `init()`: load input (`.fdtd.json` via smbjson, or legacy `.fdtd` NFDE format) - `launch()`: run the time-stepping loop - `end()`: finalize and write outputs 3. Time-step loop in `src_main_pub/timestepping.F90`: - - Update E-fields → apply materials, boundary conditions, wire coupling - - Update H-fields → apply MTLN/SPICE if enabled - - Sample observation probes, write snapshots + - Update E-fields, apply materials, boundary conditions, and wire coupling + - Update H-fields, apply MTLN/SPICE if enabled + - Sample observation probes and write snapshots ### Key Source Directories -- `src_main_pub/` — core solver, time-stepping, preprocessing, geometry, main types -- `src_conformal/` — conformal mapping (staircase reduction) -- `src_mtln/` — MTLN circuit/transmission-line solver and ngspice coupling -- `src_json_parser/` — `.fdtd.json` input format parser -- `src_wires_pub/` — wire/thin-wire models -- `external/` — submodules: `json-fortran`, `fhash`, `googletest`, `ngspice`, `lapack` +- `src_main_pub/` - core solver, time-stepping, preprocessing, geometry, main types +- `src_conformal/` - conformal mapping (staircase reduction) +- `src_mtln/` - MTLN circuit/transmission-line solver and ngspice coupling +- `src_json_parser/` - `.fdtd.json` input format parser +- `src_wires_pub/` - wire/thin-wire models +- `external/` - submodules: `json-fortran`, `fhash`, `googletest`, `ngspice`, `lapack` ### Input/Output - **Input**: `.fdtd.json` (primary — see `doc/fdtdjson.md`) or legacy `.fdtd` NFDE format -- **Output**: ASCII probe `.dat` files, XDMF+HDF5 movies/snapshots, VTK (Paraview) +- **Output**: ASCII probe `.dat` files, XDMF+HDF5 movies/snapshots, and VTK; + see `doc/output.md` - Test data and example cases live under `testData/` ### Optional Features and Conditional Compilation -Many modules are only compiled when their CMake flag is enabled. The smbjson parser, MTLN solver, and HDF5 output are all conditionally compiled. MPI support wraps communication in `src_main_pub/mpicomm.F90` and is activated via the `SEMBA_FDTD_ENABLE_MPI` flag. +The smbjson parser, MTLN solver, and MPI support are conditionally compiled. +HDF5/XDMF output is required. +MPI communication is implemented in `src_main_pub/mpicomm.F90` and activated +with `SEMBA_FDTD_ENABLE_MPI`. ## Platform Notes @@ -120,3 +133,7 @@ Many modules are only compiled when their CMake flag is enabled. The smbjson par ## Contributing From `CONTRIBUTING.md`: PRs must pass both unit tests and Python integration tests. AI-generated code is allowed but the contributor is responsible for its correctness. New functionality should include corresponding tests. + +## Commit Guidance + +For all commit-related work, use `.agents/commit-format/SKILL.md` as the primary reference. Follow its workflow for workspace analysis, atomic semantic commits, conventional commit messages, staging, and post-commit verification. diff --git a/CMakeLists.txt b/CMakeLists.txt index 285989629..791f2576f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,6 @@ cmake_minimum_required (VERSION 3.15) -project(semba-fdtd Fortran) -enable_language (Fortran) - +project(semba-fdtd LANGUAGES Fortran C) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) @@ -13,27 +11,44 @@ message(STATUS "Compiler Id is: ${CMAKE_Fortran_COMPILER_ID}") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") option(SEMBA_FDTD_ENABLE_MPI "Use MPI" OFF) -option(SEMBA_FDTD_ENABLE_HDF "Use HDF" ON) option(SEMBA_FDTD_ENABLE_MTLN "Use MTLN" ON) option(SEMBA_FDTD_ENABLE_SMBJSON "Use smbjson" ON) option(SEMBA_FDTD_ENABLE_DOUBLE_PRECISION "Use double precision (CompileWithReal8)" OFF) option(SEMBA_FDTD_ENABLE_TEST "Compile tests" ON) -option(SEMBA_FDTD_ENABLE_COVERAGE "Enable gcov coverage instrumentation (GNU only)" OFF) -option(SEMBA_FDTD_ENABLE_INTEL_XHOST_OPTIMIZATION "When compiling in Release, enables the -xHost optimization flag (not supported in github actions)" OFF) +if(SEMBA_FDTD_ENABLE_MPI AND SEMBA_FDTD_ENABLE_TEST) + set(SEMBA_FDTD_OUTPUT_MPI_TEST_RANKS 3 CACHE STRING + "Number of ranks used by distributed output integration tests") +endif() + +if(SEMBA_FDTD_ENABLE_TEST OR XDMF_HDF5_BUILD_TESTING) + enable_testing() +endif() + +option(SEMBA_FDTD_ENABLE_INTEL_XHOST_OPTIMIZATION "When compiling in Release, enables the -xHost optimization flag (not supported in github actions)" OFF) option(SEMBA_FDTD_ENABLE_INTEL_IPO "When compiling in Release, enables the interprocedural optimization" OFF) -option(SEMBA_FDTD_EXECUTABLE "Compiles executable" ON) -option(SEMBA_FDTD_MAIN_LIB "Compiles main library" ON) -option(SEMBA_FDTD_COMPONENTS_LIB "Compiles components library" ON) -option(SEMBA_FDTD_OUTPUTS_LIB "Compiles outputs library" ON) +if(SEMBA_FDTD_ENABLE_MPI) + set(HDF5_PREFER_PARALLEL TRUE) +endif() + +set(XDMF_HDF5_ENABLE_MPI "${SEMBA_FDTD_ENABLE_MPI}") + # Compilation defines. if(CMAKE_BUILD_TYPE MATCHES "Release" OR CMAKE_BUILD_TYPE MATCHES "release" ) add_definitions(-DCompileWithRelease) else() add_definitions(-DCompileWithDebug) endif() +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_compile_definitions(__WIN32__) +elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + add_compile_definitions(__APPLE__) +elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_compile_definitions(__linux__) +endif() + if(SEMBA_FDTD_ENABLE_SMBJSON) add_definitions(-DCompileWithSMBJSON) endif() @@ -45,12 +60,20 @@ if (SEMBA_FDTD_ENABLE_DOUBLE_PRECISION) else() add_definitions(-DCompileWithReal4) endif() -add_definitions( - -DCompileWithInt2 +add_definitions( + -DCompileWithInt2 -DCompileWithOpenMP ) +if(CMAKE_Fortran_COMPILER_ID MATCHES "GNU") + add_definitions(-DGNUCompiler) +endif() +if(CMAKE_Fortran_COMPILER_ID MATCHES "IntelLLVM") + add_definitions(-DIFXCompiler) +endif() + include("${CMAKE_CURRENT_SOURCE_DIR}/set_precompiled_libraries.cmake") +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/solver_configuration_summary.cmake") if (CMAKE_SYSTEM_NAME MATCHES "Linux") message(STATUS "Using Linux flags") @@ -58,6 +81,9 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux") if(CMAKE_Fortran_COMPILER_ID MATCHES "GNU") message(STATUS "Using GNU flags") + set(CMAKE_C_STANDARD 17) + set(CMAKE_C_STANDARD_REQUIRED ON) + set(CMAKE_C_EXTENSIONS ON) set(CMAKE_CXX_FLAGS "-fopenmp") set(CMAKE_Fortran_FLAGS "-fopenmp -ffree-form -ffree-line-length-none -fdec -fallow-argument-mismatch") @@ -80,10 +106,10 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux") elseif(CMAKE_Fortran_COMPILER_ID MATCHES "IntelLLVM") message(STATUS "Using IntelLLVM (ifx) flags") - + set(CMAKE_CXX_FLAGS "-qopenmp") - set(CMAKE_Fortran_FLAGS "-qopenmp -fpp -static-intel") - + set(CMAKE_Fortran_FLAGS "-qopenmp -fpp") + if (CMAKE_BUILD_TYPE STREQUAL "Release") set(CMAKE_C_FLAGS_RELEASE "-O3") set(CMAKE_CXX_FLAGS_RELEASE "-O3") @@ -126,6 +152,12 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "Windows") message(STATUS "Using Windows flags") if(CMAKE_Fortran_COMPILER_ID MATCHES "IntelLLVM") message(STATUS "Using IntelLLVM (ifx) flags") + # MSVC links executables with a 1 MiB stack by default, which is too small + # for HDF output initialisation with the Intel Fortran runtime. + add_link_options(/STACK:67108864) + # HDF output creates size-dependent Fortran temporaries. Keep them off the + # limited Windows thread stack even when the final executable uses a larger stack. + add_compile_options($<$:/heap-arrays>) # This prevents a compilation error happening due to visual studio headers being too strict. # It can possibly can be removed in the future. @@ -169,7 +201,9 @@ add_library(semba-reports "src_main_pub/errorreport.F90" "src_main_pub/snapxdmf.F90" ) -target_link_libraries(semba-reports semba-types ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES}) +target_link_libraries(semba-reports semba-types XDMF::HDF5) + +add_subdirectory(src_utils) if(SEMBA_FDTD_ENABLE_SMBJSON) add_subdirectory(src_json_parser) @@ -190,6 +224,10 @@ if (SEMBA_FDTD_ENABLE_MTLN) endif() endif() +add_subdirectory(src_output) +set(OUTPUT_LIBRARIES fdtd-output) +set(VTK_API_LIBRARIES vtkAPI) + add_subdirectory(src_conformal) set(CONFORMAL_LIBRARIES conformal) @@ -198,83 +236,76 @@ if (SEMBA_FDTD_ENABLE_TEST) add_subdirectory(test) endif() -if(SEMBA_FDTD_COMPONENTS_LIB) - add_library(semba-components - "src_main_pub/anisotropic.F90" - "src_main_pub/borderscpml.F90" - "src_main_pub/bordersmur.F90" - "src_main_pub/bordersother.F90" - "src_main_pub/electricdispersive.F90" - "src_main_pub/magneticdispersive.F90" - "src_main_pub/nodalsources.F90" - "src_main_pub/planewaves.F90" - "src_main_pub/pml_bodies.F90" - "src_main_pub/maloney_nostoch.F90" - "src_main_pub/lumped.F90" - "src_main_pub/dmma_thin_slot.F90" - "src_main_pub/farfield.F90" - "src_wires_pub/wires.F90" - "src_wires_pub/wires_mtln.F90" - ) - target_link_libraries(semba-components semba-types semba-reports ${MTLN_LIBRARIES}) -endif() -if(SEMBA_FDTD_OUTPUTS_LIB) - add_library(semba-outputs - "src_main_pub/mpicomm.F90" - "src_main_pub/observation.F90" - "src_main_pub/vtk.F90" - "src_main_pub/xdmf.F90" - "src_main_pub/xdmf_h5.F90" - ) - target_link_libraries(semba-outputs - semba-components - ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES} - ${MPI_Fortran_LIBRARIES}) -endif() -if(SEMBA_FDTD_MAIN_LIB) - # Prepares variables containing information on commit-hash and build flags. - # This information is only available at the configuration stage. - set(PROGRAM_NAME "semba-fdtd") - include("${CMAKE_CURRENT_SOURCE_DIR}/get_commit_info.cmake") - configure_file( +add_library(semba-components + "src_main_pub/anisotropic.F90" + "src_main_pub/borderscpml.F90" + "src_main_pub/bordersmur.F90" + "src_main_pub/bordersother.F90" + "src_main_pub/electricdispersive.F90" + "src_main_pub/magneticdispersive.F90" + "src_main_pub/nodalsources.F90" + "src_main_pub/planewaves.F90" + "src_main_pub/pml_bodies.F90" + "src_main_pub/maloney_nostoch.F90" + "src_main_pub/lumped.F90" + "src_main_pub/dmma_thin_slot.F90" + "src_main_pub/farfield.F90" + "src_wires_pub/wires.F90" + "src_wires_pub/wires_mtln.F90" +) +target_link_libraries(semba-components semba-types semba-reports ${MTLN_LIBRARIES}) + +add_library(semba-outputs + "src_main_pub/mpicomm.F90" +) +target_link_libraries(semba-outputs + semba-components + ${MPI_Fortran_LIBRARIES}) + +# Prepares variables containing information on commit-hash and build flags. +# This information is only available at the configuration stage. +set(PROGRAM_NAME "semba-fdtd") +include("${CMAKE_CURRENT_SOURCE_DIR}/get_commit_info.cmake") +configure_file( ${CMAKE_SOURCE_DIR}/src_main_pub/version.F90.in ${CMAKE_SOURCE_DIR}/src_main_pub/version.F90 @ONLY - ) - - add_library(semba-main - "src_main_pub/semba_fdtd.F90" - "src_main_pub/calc_constants.F90" - "src_main_pub/nfde_rotate.F90" - "src_main_pub/EpsMuTimeScale.F90" - "src_main_pub/getargs.F90" - "src_main_pub/healer.F90" - "src_main_pub/preprocess_geom.F90" - "src_main_pub/storegeom.F90" - "src_main_pub/version.F90" - "src_main_pub/postprocess.F90" - "src_main_pub/interpreta_switches.F90" - "src_main_pub/resuming.F90" - "src_main_pub/timestepping.F90" - ) - target_link_libraries(semba-main - semba-outputs - ${SMBJSON_LIBRARIES} - ${MTLN_LIBRARIES}) -endif() +) -if (SEMBA_FDTD_EXECUTABLE) - add_executable(semba-fdtd - "src_main_pub/launcher.F90" - ) - target_link_libraries(semba-fdtd semba-main semba-reports) - target_link_libraries(semba-fdtd ${MPI_Fortran_LIBRARIES}) -endif() +add_library(semba-main + "src_main_pub/semba_fdtd.F90" + "src_main_pub/calc_constants.F90" + "src_main_pub/nfde_rotate.F90" + "src_main_pub/EpsMuTimeScale.F90" + "src_main_pub/getargs.F90" + "src_main_pub/healer.F90" + "src_main_pub/preprocess_geom.F90" + "src_main_pub/storegeom.F90" + "src_main_pub/version.F90" + "src_main_pub/interpreta_switches.F90" + "src_main_pub/resuming.F90" + "src_main_pub/timestepping.F90" +) +target_link_libraries(semba-main + semba-outputs + fdtd-utils + ${OUTPUT_LIBRARIES} + ${VTK_API_LIBRARIES} + ${SMBJSON_LIBRARIES} + ${MTLN_LIBRARIES}) + +add_executable(semba-fdtd + "src_main_pub/launcher.F90" +) +target_link_libraries(semba-fdtd semba-main semba-reports) +target_link_libraries(semba-fdtd ${MPI_Fortran_LIBRARIES}) include_directories(${CMAKE_BINARY_DIR}/mod) include_directories(${HDF5_INCLUDE_DIRS}) include_directories(${FHASH_INCLUDES}) + +semba_print_configuration_summary() diff --git a/CMakePresets.json b/CMakePresets.json index 3f068ff06..f40ad9be1 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -6,7 +6,8 @@ "generator": "Ninja", "binaryDir": "build-rls/", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "SEMBA_FDTD_ENABLE_MPI": "OFF" } }, { @@ -17,6 +18,22 @@ "CMAKE_BUILD_TYPE": "Debug" } }, + { + "name": "rls-mpi", + "inherits": "rls", + "binaryDir": "build-rls-mpi/", + "cacheVariables": { + "SEMBA_FDTD_ENABLE_MPI": "ON" + } + }, + { + "name": "dbg-mpi", + "inherits": "dbg", + "binaryDir": "build-dbg-mpi/", + "cacheVariables": { + "SEMBA_FDTD_ENABLE_MPI": "ON" + } + }, { "name": "dbg-nomtln", "inherits": "dbg", @@ -57,6 +74,52 @@ "cacheVariables": { "SEMBA_FDTD_ENABLE_MTLN": "OFF" } + }, + { + "name": "intel-dbg", + "inherits": "intel-rls", + "binaryDir": "build-intel-dbg/", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + } + ], + "buildPresets": [ + { + "name": "rls", + "configurePreset": "rls" + }, + { + "name": "dbg", + "configurePreset": "dbg" + }, + { + "name": "rls-mpi", + "configurePreset": "rls-mpi" + }, + { + "name": "dbg-mpi", + "configurePreset": "dbg-mpi" + }, + { + "name": "rls-nomtln", + "configurePreset": "rls-nomtln" + }, + { + "name": "dbg-nomtln", + "configurePreset": "dbg-nomtln" + }, + { + "name": "intel-rls", + "configurePreset": "intel-rls" + }, + { + "name": "intel-rls-nomtln", + "configurePreset": "intel-rls-nomtln" + }, + { + "name": "intel-dbg", + "configurePreset": "intel-dbg" } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38f8a05c7..1db7e571a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,8 +36,8 @@ Before being merged PRs must: ## Development environment -The project uses CMake and Fortran (with optional MPI, HDF5 and MTLN -support). For details about compilation, pre‑compiled libraries and +The project uses CMake and Fortran, requires HDF5, and supports optional MPI +and MTLN. For details about compilation, pre‑compiled libraries and platform‑specific notes, refer to: - `doc/development.md` @@ -45,8 +45,8 @@ platform‑specific notes, refer to: In short: - Always update submodules before configuring CMake. -- Use a separate `build/` directory (as in the examples in - `doc/development.md`). +- Use a separate build directory. CMake presets select one automatically, for + example `build-rls/`; manual `cmake -B build` configurations use `build/`. - Prefer reproducible build configurations by passing the same CMake options you expect CI to use. @@ -69,6 +69,8 @@ pytest test/ ``` (You can also use `python -m pytest test/`.) +See the [testing guide](doc/testing.md) for build selection, test markers, +MPI execution, and focused test commands. ## Making changes @@ -87,8 +89,11 @@ Before opening a pull request: - Build the project using CMake (see `doc/development.md`). - Run unit tests, if they apply to your changes. For example: - - `build/bin/fdtd_tests` (depending on your setup). - - `pytest test/` for Python tests. + - `build-rls/bin/fdtd_tests` after building with the `rls` preset. + - `pytest test/` for Python tests after building a compatible preset. Set + `SEMBA_EXE=$PWD/build-rls/bin/semba-fdtd` to select a specific binary. + - `build/bin/...` remains correct after an explicit `cmake -B build` + configuration. - If you add new functionality, add or update tests when possible. ## Commit and pull request guidelines @@ -134,4 +139,4 @@ If you are not yet ready to contribute code, you can still help by: - Proposing enhancements or new features, including motivation and potential use cases. -Thank you again for contributing to fdtd! \ No newline at end of file +Thank you again for contributing to fdtd! diff --git a/Dockerfile b/Dockerfile index cf0830e4b..8ecfe0ca2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,58 +1,207 @@ -# ─── Stage 1: Builder ───────────────────────────────────────────────────────── -FROM ubuntu:22.04@sha256:eb29ed27b0821dca09c2e28b39135e185fc1302036427d5f4d70a41ce8fd7659 AS builder +# syntax=docker/dockerfile:1.7 +# Keep shared build dependencies first so both environments reuse this layer. +FROM ubuntu:26.04@sha256:b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e AS quality-base + +# Prevent package installation from prompting during image builds. ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y \ +# Cache apt metadata and packages between BuildKit builds without retaining them +# in the final image layer. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y \ + locales \ gfortran \ g++ \ cmake \ make \ + ninja-build \ libhdf5-dev \ libopenmpi-dev \ python3 \ python3-pip \ + python3-venv \ + paraview \ + && locale-gen en_US.UTF-8 \ + && rm -rf /var/lib/apt/lists/* + +# Use a UTF-8 locale consistently for compiler, test, and terminal output. +ENV LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 + +# These can be overridden to match the host user and avoid bind-mount ownership +# mismatches when running through Docker Compose. +ARG USERNAME=developer +ARG USER_UID=1000 +ARG USER_GID=1000 + +# Reuse a pre-existing UID/GID where Ubuntu provides one, otherwise create the +# requested account. +RUN set -eux; \ + existing_group="$(getent group ${USER_GID} | cut -d: -f1)"; \ + if [ -n "${existing_group}" ] && [ "${existing_group}" != "${USERNAME}" ] && ! getent group ${USERNAME} >/dev/null; then \ + groupmod -n ${USERNAME} ${existing_group}; \ + elif [ -z "${existing_group}" ]; then \ + groupadd --gid ${USER_GID} ${USERNAME}; \ + fi; \ + existing_user="$(getent passwd ${USER_UID} | cut -d: -f1)"; \ + if [ -n "${existing_user}" ] && [ "${existing_user}" != "${USERNAME}" ]; then \ + usermod -l ${USERNAME} -d /home/${USERNAME} -m ${existing_user}; \ + elif [ -z "${existing_user}" ]; then \ + useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USERNAME}; \ + fi; \ + mkdir -p /home/${USERNAME}/workspaces/fdtd; \ + chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}/workspaces + +# Minimal environment for compiling and running the project. +FROM quality-base AS quality + +ENV HOME=/home/${USERNAME} + +USER ${USERNAME} +WORKDIR /home/${USERNAME}/workspaces/fdtd + +# Full development environment. This target deliberately follows quality so its +# build reuses every compiler and ParaView layer. +FROM quality AS dev + +USER root + +# Development-only tools: version control, debugging, and the OpenCode CLI. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y \ + git \ + gh \ + openssh-client \ + nodejs \ + npm \ gdb \ gdbserver \ + sudo \ && rm -rf /var/lib/apt/lists/* -WORKDIR /src -COPY . . +# Preserve package-manager caches across BuildKit builds. +RUN --mount=type=cache,target=/root/.npm \ + npm install -g opencode-ai -# Install Python test/wrapper dependencies -RUN python3 -m pip install --no-cache-dir -r requirements.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + python3 -m pip install --break-system-packages fortls fprettify -# Build (MPI off by default; override at build time with --build-arg ENABLE_MPI=ON) -ARG ENABLE_MPI=OFF -ARG ENABLE_MTLN=ON -ARG BUILD_TYPE=Release +# Prepare mount points and grant passwordless sudo for interactive development. +RUN mkdir -p /home/${USERNAME}/.config \ + /home/${USERNAME}/.ssh \ + /home/${USERNAME}/.local/share/opencode \ + && chmod 700 /home/${USERNAME}/.ssh \ + && chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}/.config \ + /home/${USERNAME}/.ssh \ + /home/${USERNAME}/.local \ + && usermod -aG sudo ${USERNAME} \ + && echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USERNAME} \ + && chmod 0440 /etc/sudoers.d/${USERNAME} -RUN cmake -S . -B build \ - -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ - -DSEMBA_FDTD_ENABLE_MPI=${ENABLE_MPI} \ - -DSEMBA_FDTD_ENABLE_HDF=ON \ - -DSEMBA_FDTD_ENABLE_MTLN=${ENABLE_MTLN} \ - -DSEMBA_FDTD_ENABLE_SMBJSON=ON \ - -DSEMBA_FDTD_ENABLE_TEST=ON \ - && cmake --build build -j$(nproc) +USER ${USERNAME} +WORKDIR /home/${USERNAME}/workspaces/fdtd -# ─── Stage 2: Runtime ───────────────────────────────────────────────────────── -# Minimal image with only the shared libraries the binary needs at runtime. -# HDF5, LAPACK, BLAS, and ngspice are all statically linked on Linux, -# so only the Fortran/OpenMP runtimes and HDF5 transitive deps are required. -FROM ubuntu:22.04@sha256:eb29ed27b0821dca09c2e28b39135e185fc1302036427d5f4d70a41ce8fd7659 AS runtime +# Intel oneAPI 2025.0 ships the compilers, Intel MPI, and matching runtime +# libraries as a tested toolkit rather than relying on Intel's APT repository. +FROM intel/oneapi-hpckit:2025.0.0-0-devel-ubuntu24.04 AS intel-quality + +USER root ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ - libgfortran5 \ - libgomp1 \ - zlib1g \ - libaec2 \ + locales \ + cmake \ + make \ + ninja-build \ + python3 \ + python3-pip \ + python3-venv \ + libegl1 \ + libgl1 \ + && locale-gen en_US.UTF-8 \ + && rm -rf /var/lib/apt/lists/* + +ENV LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 + +ARG USERNAME=developer +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN set -eux; \ + existing_group="$(getent group ${USER_GID} | cut -d: -f1)"; \ + if [ -n "${existing_group}" ] && [ "${existing_group}" != "${USERNAME}" ] && ! getent group ${USERNAME} >/dev/null; then \ + groupmod -n ${USERNAME} ${existing_group}; \ + elif [ -z "${existing_group}" ]; then \ + groupadd --gid ${USER_GID} ${USERNAME}; \ + fi; \ + existing_user="$(getent passwd ${USER_UID} | cut -d: -f1)"; \ + if [ -n "${existing_user}" ] && [ "${existing_user}" != "${USERNAME}" ]; then \ + usermod -l ${USERNAME} -d /home/${USERNAME} -m ${existing_user}; \ + elif [ -z "${existing_user}" ]; then \ + useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USERNAME}; \ + fi; \ + mkdir -p /home/${USERNAME}/workspaces/fdtd; + +COPY requirements.txt /tmp/requirements.txt +RUN python3 -m venv /home/${USERNAME}/workspaces/fdtd/.venv \ + && /home/${USERNAME}/workspaces/fdtd/.venv/bin/pip install --no-cache-dir -r /tmp/requirements.txt \ + && rm /tmp/requirements.txt + +COPY docker/intel-quality-entrypoint.sh /usr/local/bin/intel-quality-entrypoint +RUN chmod 0755 /usr/local/bin/intel-quality-entrypoint +COPY . /home/${USERNAME}/workspaces/fdtd +RUN chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}/workspaces +USER ${USERNAME} +WORKDIR /home/${USERNAME}/workspaces/fdtd +ENV VIRTUAL_ENV=/home/${USERNAME}/workspaces/fdtd/.venv +ENV PATH=${VIRTUAL_ENV}/bin:${PATH} + +ENTRYPOINT ["/usr/local/bin/intel-quality-entrypoint"] +CMD ["bash", "-l"] + +# Intel development environment kept separate from the GNU-based dev target. +# The inherited entrypoint enables oneAPI and Intel MPI for every shell. +FROM intel-quality AS intel-dev + +USER root + +RUN apt-get update && apt-get install -y \ + git \ + gh \ + openssh-client \ + nodejs \ + npm \ + gdb \ + gdbserver \ + paraview \ + sudo \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /src/build/bin/semba-fdtd /usr/local/bin/semba-fdtd +RUN --mount=type=cache,target=/root/.npm \ + npm install -g opencode-ai + +RUN --mount=type=cache,target=/root/.cache/pip \ + python3 -m pip install --break-system-packages fortls fprettify + +RUN mkdir -p /home/${USERNAME}/.config \ + /home/${USERNAME}/.ssh \ + /home/${USERNAME}/.local/share/opencode \ + && chmod 700 /home/${USERNAME}/.ssh \ + && chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}/.config \ + /home/${USERNAME}/.ssh \ + /home/${USERNAME}/.local \ + && usermod -aG sudo ${USERNAME} \ + && echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USERNAME} \ + && chmod 0440 /etc/sudoers.d/${USERNAME} -WORKDIR /work +ENV HOME=/home/${USERNAME} -ENTRYPOINT ["semba-fdtd", "-i"] +USER ${USERNAME} +WORKDIR /home/${USERNAME}/workspaces/fdtd diff --git a/README.md b/README.md index 45101bfd1..557ef4b98 100755 --- a/README.md +++ b/README.md @@ -39,18 +39,30 @@ In a nutshell, semba-fdtd capabilities are # Usage -Compilation and debugging instructions can be found [here](doc/development.md). +Compilation and debugging instructions are in the +[development guide](doc/development.md). -A short tutorial on usage can be found [here](doc/tutorials/veritasium/veritasium.md). +A short tutorial is available in the +[Veritasium example](doc/tutorials/veritasium/veritasium.md). -The main binary is `semba-fdtd` which uses the `.fdtd.json` format, [described here](doc/fdtdjson.md), as input files. +The main binary is `semba-fdtd`, which uses the `.fdtd.json` +[input format](doc/fdtdjson.md). It can be run with ```shell semba-fdtd -i CASE_NAME.fdtd.json ``` -Tests must be run from the root folder. `python` wrapper test assumes that `semba-fdtd` has been compiled successfully and is located in folder `build/bin/`. For intel compilation it also assumes that the intel runtime libraries are accessible. +See the [output guide](doc/output.md) for probe artifact formats and +the [testing guide](doc/testing.md) for native, Python, and MPI test commands. + +Tests must be run from the root folder. Python tests automatically select the +first compatible preset binary, preferring Release builds, based on +`SEMBA_FDTD_ENABLE_MPI` and `SEMBA_FDTD_ENABLE_MTLN`, and then fall back to an +explicitly configured `build/bin/semba-fdtd` binary. +Set `SEMBA_EXE` to select a particular executable, for example +`SEMBA_EXE=$PWD/build-rls/bin/semba-fdtd pytest test/`. +For Intel compilation, the Intel runtime libraries must be accessible. # License diff --git a/cmake/solver_configuration_summary.cmake b/cmake/solver_configuration_summary.cmake new file mode 100644 index 000000000..cc974f0d1 --- /dev/null +++ b/cmake/solver_configuration_summary.cmake @@ -0,0 +1,35 @@ +function(semba_print_configuration_summary) + message(STATUS "") + message(STATUS "Solver configuration summary:") + message(STATUS " System: ${CMAKE_SYSTEM_NAME}") + message(STATUS " Generator: ${CMAKE_GENERATOR}") + message(STATUS " Build type: ${CMAKE_BUILD_TYPE}") + message(STATUS " Fortran compiler: ${CMAKE_Fortran_COMPILER} (${CMAKE_Fortran_COMPILER_ID} ${CMAKE_Fortran_COMPILER_VERSION})") + message(STATUS " C compiler: ${CMAKE_C_COMPILER} (${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION})") + message(STATUS " Runtime output: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") + message(STATUS " Library output: ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") + message(STATUS " Fortran modules: ${CMAKE_Fortran_MODULE_DIRECTORY}") + message(STATUS " MPI: ${SEMBA_FDTD_ENABLE_MPI}") + message(STATUS " MTLN: ${SEMBA_FDTD_ENABLE_MTLN}") + message(STATUS " SMBJSON: ${SEMBA_FDTD_ENABLE_SMBJSON}") + message(STATUS " Tests: ${SEMBA_FDTD_ENABLE_TEST}") + + if(SEMBA_FDTD_ENABLE_DOUBLE_PRECISION) + message(STATUS " Precision: double") + else() + message(STATUS " Precision: single") + endif() + + if(SEMBA_FDTD_ENABLE_MPI) + message(STATUS " MPI Fortran compiler: ${MPI_Fortran_COMPILER}") + message(STATUS " MPI version: ${MPI_Fortran_VERSION}") + endif() + + message(STATUS " HDF5 version: ${HDF5_VERSION}") + message(STATUS " HDF5 parallel: ${HDF5_IS_PARALLEL}") + message(STATUS " HDF5 Fortran libraries: ${HDF5_Fortran_LIBRARIES}") + + if(SEMBA_FDTD_ENABLE_MTLN) + message(STATUS " LAPACK directory: ${LAPACK_DIR}") + endif() +endfunction() diff --git a/doc/development.md b/doc/development.md index a4dd8aa4b..0cd2da100 100644 --- a/doc/development.md +++ b/doc/development.md @@ -1,5 +1,22 @@ # Compilation and debugging +## Contents + +- [Prebuilt binary releases](#running-from-prebuilt-binary-releases) +- [GNU/Linux compilation](#gnulinux-compilation) + - [Compilation options](#compilation-options) + - [HDF5 libraries](#hdf5-libraries) + - [MTLN and ngspice](#mtln-and-ngspice) + - [MPI](#mpi) +- [Windows (intelLLVM) compilation](#windows-intelllvm-compilation) + - [Prerequisites](#prerequisites) + - [Compilation process](#compilation-process) + - [Visual Studio debugging](#debugging-with-visual-studio) +- [WSL2 and Visual Studio Code setup](#wsl2--visual-studio-code--gfortran-setup-guide) +- [Debugging the project](#debugging-the-project) + - [MPI debugging](#debugging-with-mpi) + - [Troubleshooting](#troubleshooting) + ## Running from prebuilt binary releases Prebuilt binares are available at [releases](https://github.com/OpenSEMBA/fdtd/releases). @@ -8,7 +25,16 @@ In windows, you need to install [intel oneapi runtime libraries](https://www.int ## GNU/Linux Compilation -It is important to point out the repository has dependencies which are available as submodules. It is necessary to run `git submodule init` and `git submodule update` from the root folder before running any `cmake` or `build` commands. +The repository has dependencies available as submodules. Before running CMake, initialise them from the repository root: + +```shell +git submodule update --init --recursive +``` + +CMake presets use separate build directories, such as `build-rls/` and +`build-dbg-mpi/`, so their configurations can coexist. +Run CMake with `--fresh` when changing the options of an existing build +directory so its previous cache is discarded. If you use intel oneapi compiler, make sure to run @@ -23,24 +49,61 @@ If you use intel oneapi compiler, make sure to run #### HDF5 Libraries -HDF5 precompiled libraries for ubuntu are used by default (see [precompiled libraries cmake script](../set_precompiled_libraries.cmake)). +GNU builds use the system HDF5 installation by default. +Intel builds use the bundled serial HDF5 installation unless a different +installation is selected with `HDF5_ROOT` or `HDF5_DIR`. + +You can compile HDF5 for your platform by downloading the latest sources from +the [HDF5 website](https://www.hdfgroup.org/downloads/hdf5/source-code/). +Extract the archive, then build and install a serial version with: + +```shell +cmake -S . -B build \ + -DHDF5_BUILD_FORTRAN=ON \ + -DHDF5_ENABLE_Z_LIB_SUPPORT=NO \ + --fresh +cmake --build build -j +cmake --install build --prefix ~/hdf5-installed +``` + +A specific HDF5 installation can be selected with +`-DHDF5_ROOT=`: -You can compile HDF5 for your specific platform downloading the latest sources from this [link](https://www.hdfgroup.org/downloads/hdf5/source-code/). -Extract to a folder and build and install with the following commands +```shell +cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DHDF5_ROOT=~/hdf5-installed \ + -DHDF5_USE_STATIC_LIBRARIES=TRUE \ + --fresh +cmake --build build -j +``` + +Parallel movie output requires HDF5 built with both MPI and Fortran support. +Configure HDF5 with MPI compiler wrappers and `HDF5_ENABLE_PARALLEL=ON`: ```shell - cmake -S . -B build -DHDF5_BUILD_FORTRAN=ON -DHDF5_ENABLE_Z_LIB_SUPPORT=NO --fresh - cmake --build build -j - cmake --install build --prefix ~/hdf5-installed +CC=mpicc FC=mpifort cmake -S . -B build-hdf5-parallel \ + -DHDF5_BUILD_FORTRAN=ON \ + -DHDF5_ENABLE_PARALLEL=ON \ + -DHDF5_ENABLE_Z_LIB_SUPPORT=NO +cmake --build build-hdf5-parallel -j +cmake --install build-hdf5-parallel --prefix ~/hdf5-parallel ``` -A specific HDF5 library can be set with the option `-DHDF5_ROOT=`, e.g. +MPI builds automatically prefer parallel HDF5 when it is available. +Select the parallel HDF5 installation while enabling MPI: ```shell - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DHDF5_ROOT=~/hdf5-installed -DHDF5_USE_STATIC_LIBRARIES=TRUE --fresh - cmake --build build -j +cmake -S . -B build-parallel \ + -DSEMBA_FDTD_ENABLE_MPI=ON \ + -DHDF5_ROOT=~/hdf5-parallel ``` +If HDF5 reports parallel support, configuration verifies that its Fortran MPIO +interfaces compile and link with the selected MPI library. +MPI builds using serial HDF5 remain valid, but the parallel HDF5 movie backend +is not available in those builds. + #### MTLN and ngspice MTLN depends on `lapack` and `ngspice`. Precompiled versions are included for windows (intelLLVM) and ubuntu (intelLLVM and GNU). @@ -93,8 +156,13 @@ navigate to the `/fdtd/` folder that has been created, this folder will be refer ### Prerequisites This compilation process will use the already available precompiled libraries included with the project, thus it's not required to build them manually. -This repository has dependencies that are available as submodules. It is necessary to run `git submodule init` and `git submodule update` from the root folder before running any `cmake` or `build` commands. -In the .gitmodules file, the submodules use the SSH remote URL by default. If not using a SSH-key in the computer where the following process will be performed, the remote addresses for each submodule must be individually changed to their HTTPS alternative. +This repository has dependencies available as submodules. Initialise them from the root folder before running CMake: + +```shell +git submodule update --init --recursive +``` + +The default submodule URLs use HTTPS. This software requires [Windows BaseKit](https://www.intel.com/content/www/us/en/developer/tools/oneapi/base-toolkit.html) and [Windows HPCKit](https://www.intel.com/content/www/us/en/developer/tools/oneapi/hpc-toolkit-download.html). Install these packages with all their features selected. @@ -113,7 +181,7 @@ This will load the OneAPI environment for x64. Navigate to the fdtd root folder, choose between "Debug"/"Release" for `-DCMAKE_BUILD_TYPE`, and "ON"/"OFF" for `-DSEMBA_FDTD_ENABLE_MPI`, for example, a Release version with MPI Support would be: ```shell -cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=Release -DSEMBA_FDTD_ENABLE_MPI=ON +cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=Release -DSEMBA_FDTD_ENABLE_MPI=ON --fresh ``` Then, @@ -260,8 +328,7 @@ cd This project has submodule dependencies remember to initiate an update the ```bash -git submodule init -git submodule update +git submodule update --init --recursive ``` #### Step 2: Install Python Requirements @@ -329,7 +396,7 @@ An example of launch.json filke is given. This will use a file as argument when "name": "Fortran Launch (GDB)", "type": "cppdbg", "request": "launch", - "program": "${workspaceRoot}/build/bin/semba-fdtd", + "program": "${workspaceRoot}/build-dbg/bin/semba-fdtd", "miDebuggerPath": "gdb", "args": ["-i", "shieldingEffectiveness.fdtd.json"], "stopAtEntry": false, @@ -341,55 +408,247 @@ An example of launch.json filke is given. This will use a file as argument when Now you are ready to work with the project. -### Debugging with MPI +### Debugging with MPI + +#### Overview + +GDB controls one process per debug session. +To debug an MPI job, each MPI rank is therefore started under its own +`gdbserver`, and VS Code creates one `cppdbg` session for each rank. + +The checked-in configuration supports a two-rank solver job: + +```text +VS Code: MPI: debug all ranks (2 ranks) + |-- GDB session: rank 0 -> localhost:20000 -> gdbserver -> MPI rank 0 + `-- GDB session: rank 1 -> localhost:20001 -> gdbserver -> MPI rank 1 +``` + +Both sessions are shown separately in the VS Code **Call Stack** panel. +Breakpoints are sent to both sessions, although rank-specific control flow can +mean that only one rank reaches a particular breakpoint. + +#### Configuration files -gdb is a serial debugger, but can be attached to one of the parallel processes after they have started running. +| File | Responsibility | +|---|---| +| `.vscode/launch.dev.json` | Version-controlled template for debug configurations. | +| `.vscode/launch.json` | Active local configuration; ignored by Git. | +| `.vscode/settings.json` | Local input file, working directory, and test filter values. | +| `.vscode/tasks.json` | Build tasks and the MPI-safe `fdtd_tests` preparation task. | +| `scripts/debug-mpi-gdbserver.sh` | Starts MPI ranks under `gdbserver`, waits for ports, and cleans stale jobs. | +Copy the version-controlled files template when initially configuring the +workspace, or when the template changes: -1. Modify the file launch.json to attach to a running process after launching the debugger: +```shell +cp .vscode/launch.dev.json .vscode/launch.json +``` + +Add the following project-specific values to the local +`.vscode/settings.json` file: ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "(gdb) Attach", - "type": "cppdbg", - "request": "attach", - "processId": "${command:pickProcess}", - "program": "${workspaceFolder}/build/bin/semba-fdtd", - "MIMode": "gdb", - "miDebuggerPath": "/usr/bin/gdb", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "Set Disassembly Flavor to Intel", - "text": "-gdb-set disassembly-flavor intel", - "ignoreFailures": true - } - ] - } - ] + "semba-fdtd.debug.inputFile": "pw-in-box.fdtd.json", + "semba-fdtd.debug.inputCwd": "testData/cases/planewave", + "semba-fdtd.debug.mpiGtestFilter": "conformal.geometry_coord_position" } ``` -2. Use *mpirun* to execute semba-fdtd paralellized in 'np' processes: -``` -mpirun -np 2 build/bin/semba-fdtd -i input_file.fdtd.json -args +`inputFile` is relative to `inputCwd`. +Set `inputCwd` to the directory containing the JSON input and all files that +the JSON references with relative paths, such as excitation files. + +#### Build requirements + +Debug launches do not configure or rebuild the project automatically. +Build an MPI-enabled Debug executable before starting VS Code debugging: + +```shell +cmake --fresh --preset dbg-mpi +cmake --build --preset dbg-mpi -j +``` + +The `dbg-mpi` preset builds in `build-dbg-mpi/`. +Its configuration can coexist with other presets, so a Release or non-MPI +preset build does not replace the MPI Debug executable. +If the options of `dbg-mpi` itself change, rerun the commands above to refresh +that build directory. + +#### Starting all ranks + +1. Open the VS Code **Run and Debug** view. +2. Select `MPI: debug all ranks (2 ranks)`. +3. Press F5. +4. Wait for both `MPI all ranks: rank 0` and + `MPI all ranks: rank 1` to appear in **Call Stack**. +5. Continue each session once after the initial entry stop. + +The two ranks must both be allowed to continue. +If one remains stopped before `MPI_Init` or another collective operation, the +other rank can appear blocked while it waits for that rank. + +`MPI: debug solver rank 0 (2 ranks)` is a simpler alternative. +It runs a two-rank MPI job but attaches GDB only to rank 0; +rank 1 runs normally. + +#### Startup sequence + +The all-rank configuration is a VS Code compound containing two hidden launch +configurations. +No `preLaunchTask` or problem matcher is used for the solver. +Instead, the C/C++ extension directly owns the helper processes through +`debugServerPath` and waits for readiness through `serverStarted`. + +The startup sequence is: + +1. The rank 0 launch configuration invokes + `scripts/debug-mpi-gdbserver.sh` with `--foreground-all 2`. +2. The script validates `--workdir`, changes to that directory, and executes + one `mpirun -np 2` job. +3. Each MPI process calculates its debugger port as + `20000 + OMPI_COMM_WORLD_RANK` and then executes `gdbserver`. +4. The rank 0 adapter waits for `Listening on port 20000` and connects its GDB. +5. The rank 1 launch configuration invokes the same script with + `--wait-for-port 20001` instead of starting a second MPI job. +6. The waiter checks `/proc/net/tcp` and `/proc/net/tcp6` without opening a + debugger connection. +7. When port 20001 is listening, the rank 1 adapter connects its own GDB. +8. The compound's `stopAll` option stops both sessions when either session is + terminated. + +It is important that rank 1 only waits for its port. +Starting `mpirun` from both hidden configurations would create two unrelated +MPI jobs rather than two debugger views of the same job. + +#### Port and rank mapping + +| MPI rank | `gdbserver` address | VS Code session | +|---:|---|---| +| 0 | `localhost:20000` | `MPI all ranks: rank 0` | +| 1 | `localhost:20001` | `MPI all ranks: rank 1` | + +The shell script supports more ranks, but the checked-in compound explicitly +defines two GDB sessions. +Supporting additional ranks requires another hidden launch configuration and +port waiter for each extra rank. + +#### Working directory + +The target process is launched by `gdbserver`, not directly by `cppdbg`. +Consequently, the `cwd` property alone does not reliably set the inferior's +working directory. + +The launch configuration passes the directory explicitly: + +```text +--workdir ${workspaceFolder}/${config:semba-fdtd.debug.inputCwd} ``` -3. Once mpirun is running, launch the debuuger. A selection box will ask which process to attach to. Type *semba-fdtd* and all mpirun processes running semba will display. Selecto which process the debugger should attach to +The script verifies that the directory exists and is writable, then changes to +it before starting `mpirun`. +This is required for relative JSON resources, output files, and solver control +files such as `running`, `pause`, `relaunch`, and `forcestop`. + +#### Script modes + +The helper script has the following modes: + +| Mode | Purpose | +|---|---| +| `--foreground-all ...` | Run every MPI rank under a separate `gdbserver`. | +| `--foreground-debug-rank ...` | Debug one rank and run the remaining ranks normally. | +| `--wait-for-port ` | Wait for another launch configuration's `gdbserver`. | +| `--debug-rank ...` | Detached preparation mode used by task-based workflows. | +| `--stop` | Stop a detached MPI debug job recorded by the script. | + +The `--foreground-*` modes are preferred for solver debugging because +`OpenDebugAD7` owns their lifetime directly. +This avoids races in which a background task exits before GDB connects. + +#### Debugging MPI unit tests + +The full `fdtd_tests` suite should normally be debugged as one process, even +when linked against an MPI-enabled build. +Several tests write fixed file names and are not safe to execute concurrently +on every rank. + +The `MPI: debug fdtd_tests (2 ranks)` compound is intended only for an +MPI-safe filtered test. +Set `semba-fdtd.debug.mpiGtestFilter` in `.vscode/settings.json` before using +that compound. #### Troubleshooting -1. After selecting the process the debugger should attach to, a new terminal opens with the message "Superuser access is required to attach to a process" +**GDB connection timeout** + +Confirm that the selected configuration is +`MPI: debug all ranks (2 ranks)` and reload the VS Code window after changing +`launch.json`. +The solver configuration must use `debugServerPath`, `serverStarted`, and the +foreground script modes; it must not depend on a background `preLaunchTask`. + +Check for stale MPI or `gdbserver` processes before retrying: + +```shell +pgrep -af 'gdbserver|prterun|mpirun' +``` + +**Cannot create `running` or another relative file** + +Verify `semba-fdtd.debug.inputCwd` and confirm that the directory is writable. +The debug output prints `MPI working directory: ...` before `mpirun` starts. + +**A breakpoint is not reached** -Run the following command as super user: +Confirm that the correct rank executes that code path and that the breakpoint +was installed before the one-time initialization code ran. +Also confirm that the active executable is an MPI-enabled Debug build. +A valid source breakpoint cannot force execution through a false runtime +condition. + +**Both sessions connect but the program does not advance** + +Select each rank in **Call Stack** and continue it. +One stopped rank can hold the other rank inside an MPI collective operation. + +**Warnings about unavailable system-library debug information** + +Messages about missing separate debug information for MPI or system libraries +are non-fatal when debugging project sources. +Install the corresponding system debug packages only when stepping inside those +libraries is required. + +#### Manual attach fallback + +The native compound is preferred, but GDB can also attach manually to an +already running process. +Start the MPI job in a terminal: + +```shell +mpirun -np 2 build-dbg-mpi/bin/semba-fdtd -i input_file.fdtd.json +``` + +Then use the `Attach to process` configuration and select one `semba-fdtd` +process. +This method provides one attached rank per debug session and does not perform +the automatic port coordination described above. + +If Linux blocks manual attachment because of `ptrace_scope`, temporarily relax +the restriction only on a trusted development machine: + +```shell +sudo sysctl kernel.yama.ptrace_scope=0 ``` -echo 0| sudo tee /proc/sys/kernel/yama/ptrace_scope + +Restore the normal restriction after debugging: + +```shell +sudo sysctl kernel.yama.ptrace_scope=1 ``` -([source](https://github.com/Microsoft/MIEngine/wiki/Troubleshoot-attaching-to-processes-using-GDB)) \ No newline at end of file + +See the [MIEngine troubleshooting guide][miengine-troubleshooting] +for more information. + +[miengine-troubleshooting]: https://github.com/Microsoft/MIEngine/wiki/Troubleshoot-attaching-to-processes-using-GDB diff --git a/doc/docker.md b/doc/docker.md index 03baae93c..c8700b6c3 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -1,245 +1,191 @@ # Docker -This document explains how to use Docker to build, test, and run semba-fdtd without installing any dependencies on your machine. +Docker provides four environments for working with semba-fdtd. +The `quality`, `dev`, and `intel-dev` environments mount the repository at +`/home/developer/workspaces/fdtd`, so build artefacts and simulation output are +written to the host workspace. +CMake presets use separate build directories, so their configurations can +coexist. For example, `rls` uses `build-rls/` and `rls-mpi` uses +`build-rls-mpi/`. +Run CMake with `--fresh` when changing the options of an existing preset build +directory so its previous cache is discarded. -## Prerequisite: initialize submodules +## Contents -Submodules must be initialized on the host before building the image, as the build depends on them: +- [Prerequisite](#prerequisite) +- [Environments](#environments) +- [Build images](#build-images) +- [Compile with quality](#compile-with-quality) +- [Compile with Intel oneAPI](#compile-with-intel-oneapi) +- [Run binaries and examples](#run-binaries-and-examples) +- [Run tests](#run-tests) +- [Development environment](#development-environment) + +## Prerequisite + +Initialise submodules before building an image: ```bash git submodule update --init --recursive ``` -## Included files +## Environments -| File | Description | -|---|---| -| `Dockerfile` | Multi-stage build: `builder` (compilation + tests) and `runtime` (binary only) | -| `docker-compose.yml` | Services `solver` (run simulations) and `test` (build and test) | -| `.dockerignore` | Excludes unnecessary files from the build context | +| Service | Purpose | Included tools | +|---|---|---| +| `quality` | Compile, test, run examples, and inspect generated output | GNU C/C++/Fortran compilers, CMake, Ninja, MPI, HDF5, Python, and ParaView | +| `intel-quality` | Validate Intel oneAPI compiler and Intel MPI builds | Intel oneAPI HPCKit, Intel MPI, CMake, Ninja, and the Intel HDF5 runtime | +| `dev` | GNU interactive development and Dev Container | Everything in `quality`, plus Git, GitHub CLI, SSH client, Node/npm, OpenCode, Fortran tools, and debuggers | +| `intel-dev` | Intel interactive development and Dev Container | Intel oneAPI HPCKit and Intel MPI, plus the development tools and GDB debugging support | ---- +`quality` intentionally excludes developer-only tools. +`quality`, `dev`, and `intel-dev` include ParaView. +`intel-quality` is for Intel-specific compilation checks; it is not the Python +test environment or the Dev Container. +`intel-dev` is an independent image and does not inherit the GNU-based `dev` +target. -## Building the images +## Build Images + +Build `quality` first: ```bash -docker compose build test # image for tests -docker compose build solver # runtime image for simulations +docker compose build quality ``` -`build` only constructs and saves the image to disk — it does not start any container. You only need to re-run it when the code changes. - -### Build arguments - -The build mode and optional features can be configured via `--build-arg`: - -| Argument | Values | Default | -|---|---|---| -| `BUILD_TYPE` | `Release`, `Debug` | `Release` | -| `ENABLE_MPI` | `ON`, `OFF` | `OFF` | -| `ENABLE_MTLN` | `ON`, `OFF` | `ON` | +Build `dev` when its additional developer tools are required: ```bash -# Debug build -docker compose build --build-arg BUILD_TYPE=Debug test - -# Combining arguments -docker compose build \ - --build-arg BUILD_TYPE=Debug \ - --build-arg ENABLE_MPI=ON \ - --build-arg ENABLE_MTLN=OFF \ - test +docker compose build dev ``` -**Release** (`-Ofast`): optimized for speed, no debug information. -**Debug** (`-g -O0 -fcheck=all -fbacktrace`): no optimization, with runtime checks and backtraces on error — useful for diagnosing crashes. +Build the Intel oneAPI validation environment when checking Intel-specific configurations: -### Base image digest - -The `Dockerfile` pins the base image using a SHA256 digest instead of just the tag: - -```dockerfile -FROM ubuntu:22.04@sha256:eb29ed27... AS builder +```bash +docker compose build intel-quality ``` -`ubuntu:22.04` is a mutable tag — Canonical can update it at any time. The digest identifies an exact, immutable image, so builds are fully reproducible regardless of when or where they run. - -The downside is that OS security patches are not picked up automatically. To update the digest: +Build the Intel development environment for interactive work or debugging: ```bash -docker pull ubuntu:22.04 -docker inspect ubuntu:22.04 --format='{{index .RepoDigests 0}}' +docker compose build intel-dev ``` -Then replace both occurrences of the digest in the `Dockerfile` (builder and runtime stages). +The `dev` target is built from `quality`. +Docker therefore reuses the compiler and ParaView layers when building `dev`. ---- +The source tree is never copied into either image. +Editing source files, running examples, and creating build directories do not invalidate image layers or require an image rebuild. -## Running the tests +## Compile With Quality -```bash -docker compose run --rm test -``` - -This runs in sequence: -1. `build/bin/fdtd_tests` — unit tests (GoogleTest) -2. `python3 -m pytest test/ --durations=20` — Python integration tests - -To run only part of the test suite: +Run CMake presets from the mounted workspace: ```bash -# Unit tests only -docker compose run --rm test build/bin/fdtd_tests - -# pytest with a specific marker -docker compose run --rm test python3 -m pytest test/ -m mtln -docker compose run --rm test python3 -m pytest test/ -m hdf +docker compose run --rm quality cmake --fresh --preset rls +docker compose run --rm quality cmake --build --preset rls ``` -To open an interactive shell inside the container: +For an MPI build: ```bash -docker compose run --rm --entrypoint bash test +docker compose run --rm quality cmake --fresh --preset rls-mpi +docker compose run --rm quality cmake --build --preset rls-mpi ``` ---- +The preset build directory, such as `build-rls/` or `build-rls-mpi/`, remains +on the host after the container exits. -## Debugging a simulation +## Compile With Intel oneAPI -This uses `gdbserver` inside the container and connects VSCode to it via the C/C++ extension. +`intel-quality` initialises Intel oneAPI and Intel MPI automatically. +It also selects the bundled Intel HDF5 runtime, so no manual environment setup is required. -### Prerequisites - -- VSCode extension: **C/C++** (`ms-vscode.cpptools`) -- `gdb` installed on the host: - ```bash - sudo apt install gdb - ``` - -### Step 1 — Build the debug image +Use the Intel Release preset to validate the MPI, MTLN, and double-precision configuration: ```bash -docker compose build debug +docker compose run --rm intel-quality cmake --fresh --preset intel-rls +docker compose run --rm intel-quality cmake --build --preset intel-rls +docker compose run --rm intel-quality build-intel-rls/bin/fdtd_tests ``` -### Step 2 — Extract the binary for local symbol loading - -VSCode's GDB client needs a local copy of the binary to load debug symbols. Extract it from the image once after each build: +For the Intel configuration without MTLN: ```bash -docker create --name tmp-debug fdtd-debug -docker cp tmp-debug:/src/build/bin/semba-fdtd ./build/bin/semba-fdtd -docker rm tmp-debug +docker compose run --rm intel-quality cmake --fresh --preset intel-rls-nomtln +docker compose run --rm intel-quality cmake --build --preset intel-rls-nomtln +docker compose run --rm intel-quality build-intel-rls-nomtln/bin/fdtd_tests ``` -### Step 3 — Start gdbserver - -Place your input files in `simulations/` and run: +Open a shell for repeated Intel build checks: ```bash -docker compose run --rm -p 2345:2345 debug case.fdtd.json +docker compose run --rm intel-quality ``` -The container starts and waits, printing something like: - -``` -Process /src/build/bin/semba-fdtd created; pid = 7 -Listening on port 2345 -``` - -### Step 4 — Connect from VSCode - -`.vscode/launch.json` is tracked in the repository and already contains the **Docker: attach gdbserver** configuration. +A terminal inside the container should appear. -Open the **Run and Debug** panel (`Ctrl+Shift+D`), select **Docker: attach gdbserver**, and press `F5`. VSCode connects, the simulation starts, and breakpoints work normally. +For a debuggable Intel build, use the separate `intel-dbg` preset: -```json -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Docker: attach gdbserver", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}/build/bin/semba-fdtd", - "miDebuggerServerAddress": "localhost:2345", - "miDebuggerPath": "gdb", - "MIMode": "gdb", - "cwd": "${workspaceFolder}/simulations", - "sourceFileMap": { - "/src": "${workspaceFolder}" - }, - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - } - ] - } - ] -} +```bash +docker compose run --rm intel-dev cmake --fresh --preset intel-dbg +docker compose run --rm intel-dev cmake --build --preset intel-dbg +docker compose run --rm intel-dev gdb build-intel-dbg/bin/semba-fdtd ``` -> The `sourceFileMap` maps `/src` (container path baked into debug info) to `${workspaceFolder}` on the host, so source files display correctly. - ---- - -## Running a simulation +## Run Binaries And Examples -Place your input files in `simulations/` (created at the repo root) and run: +Use the binary produced in the mounted build directory: ```bash -docker compose run --rm solver case.fdtd.json +docker compose run --rm quality build-rls/bin/semba-fdtd -i path/to/case.fdtd.json ``` -The `simulations/` directory is mounted at `/work` inside the container, which is the solver's working directory. - ---- - -## Managing images and containers +Any output generated by the solver is available immediately in the corresponding host directory. -### Where are images stored? - -Docker manages them internally (under `/var/lib/docker/` on Linux), not in a project folder. To inspect them: +Open an interactive quality shell when repeatedly compiling or running examples: ```bash -docker images # list all saved images -docker image prune # remove unused images +docker compose run --rm quality ``` -### Viewing active containers +ParaView is available in both environments: ```bash -docker ps # currently running containers -docker ps -a # running + stopped containers +docker compose run --rm quality paraview +docker compose run --rm dev paraview ``` -### Stopping and removing containers +## Run Tests + +Configure a build with tests, then run the required test commands: ```bash -docker stop # stops the container (does not remove it) -docker rm # removes it -docker rm -f # stop and remove in one step -docker container prune # remove all stopped containers +docker compose run --rm quality cmake --fresh --preset rls +docker compose run --rm quality cmake --build --preset rls +docker compose run --rm quality build-rls/bin/fdtd_tests ``` -The full ID is not required — the first 3–4 characters are enough: +Python integration tests require the project dependencies in a virtual environment: ```bash -docker stop a1b2 +docker compose run --rm quality python3 -m venv .venv +docker compose run --rm quality .venv/bin/python -m pip install -r requirements.txt +docker compose run --rm quality .venv/bin/python -m pytest test/ --durations=20 ``` -> With `--rm` (used in all `docker compose run` commands) the container is removed automatically when it finishes, so no manual cleanup is needed. +## Development Environment -### Does closing the terminal stop the container? - -- **Without `-d`** (normal mode, what we use): yes, closing the terminal stops the container because it is attached to it. -- **With `-d`** (detached mode): no, it keeps running in the background even after the terminal is closed. +Use `dev` for a full interactive shell: ```bash -# Run in the background -docker compose run -d --rm test +docker compose run --rm dev ``` -For tests it is better to omit `-d` so you can see the output in real time. +The Dev Container configuration also uses the `dev` service. +It mounts the host Git, SSH, GitHub CLI, and OpenCode configuration required by the development workflow. + +The Intel Dev Container is defined separately at +`.devcontainer/intel/devcontainer.json` and uses the `intel-dev` service. +Open that configuration in the Dev Containers extension to develop and debug +with `ifx`; it selects the `intel-dbg` preset by default. diff --git a/doc/fdtdjson.md b/doc/fdtdjson.md index dec86010c..e8d7e8f1f 100644 --- a/doc/fdtdjson.md +++ b/doc/fdtdjson.md @@ -5,6 +5,26 @@ Being in JSON, it can be easily navigated with most text editors, such as Visual There are also multiple tools to read and write them. This document assumes that you are familiar with the basic JSON notation, a brief explanation on this notation can be found [here](https://www.w3schools.com/js/js_json_syntax.asp). +## Contents + +- [Examples](#examples) +- [FDTD-JSON objects description](#fdtd-json-objects-description) + - [`general`, `background`, and `boundary`](#general) + - [`mesh`](#mesh) + - [`materials`](#materials) + - [`materialAssociations`](#materialassociations) + - [`probes`](#probes) + - [`sources`](#sources) +- [Material types](#bulk-materials) + - [`lumped` models](#lumped) + - [Wire and multiwire materials](#wire) + - [Terminals and connectors](#terminal) +- [Probe types](#probe-types) +- [Probe domains](#domain) +- [Source types](#planewave) + +## Examples + The following are examples of valid inputs: 1. An empty space illuminated by a plane wave: [planewave.fdtd.json](testData/input_examples/planewave.fdtd.json). The field at a point close to the center is recorded. diff --git a/doc/mtln.md b/doc/mtln.md index 57c71ee79..a1b2f8796 100644 --- a/doc/mtln.md +++ b/doc/mtln.md @@ -5,6 +5,16 @@ bibliography: # The `mtln` solver module This module allows to solve networks of multiconductor transmission line bundles in the time domain. + +## Contents + +- [Multiconductor transmission lines](#multiconductor-transmission-lines) +- [Networks](#networks) +- [Bundles](#bundles) +- [Features](#features) + - [Coupling to NgSpice](#coupling-to-ngspice) + - [Dispersive elements](#dispersive-elements) + In the context of transmission line theory, a working definition of each of the terms above is: * A **multiconductor tranmission line** is tranmission line composed of more than one conductor (and the reference conductor), i.e a tranmssion line composed of 3 or more conductors, where one of them is taken as the reference. @@ -137,4 +147,3 @@ Dispersive elements are those whose properties depend on frequency. The solver w - diff --git a/doc/output.md b/doc/output.md new file mode 100644 index 000000000..2e85a765d --- /dev/null +++ b/doc/output.md @@ -0,0 +1,59 @@ +# Probe Output + +This page describes the files generated by each probe type. +In the filename patterns below, `R` is the root of the input filename, `N` is +the probe name, `F` is the sampled field or component, `C` is the coordinate, +and `B` is the probe bounding box. + +## Scalar Text Output + +Point, wire, bulk-current, line, and far-field probes publish formatted `.dat` +files beside the originating `.fdtd.json` input. +They do not use probe-specific directories and do not create binary artifacts +or metadata sidecars. + +| Probe type | Without MPI | With MPI | +| --- | --- | --- | +| `point`, time domain | `R_N_F_C_tm.dat` | The same single `.dat` file. | +| `point`, frequency domain | `R_N_F_C_fq.dat` | The same single `.dat` file. | +| `wire`, FDTD current | `R_N_W{axis}_C_s{node}_tm.dat` | The same single `.dat` file. | +| `wire`, MTLN voltage or current | `R_N__.dat` | The same single `.dat` file, written by the selected owning rank. | +| `bulkCurrent` | `R_N_J{axis}_B_tm.dat` | The same single `.dat` file, reduced and written by rank 0. | +| `line` | `R_N_LI_tm.dat` | The same single `.dat` file, reduced and written by rank 0. | +| `farField` | `R_N_FF_B.dat` | The same single `.dat` file. | + +Point probes configured for both domains produce one `_tm.dat` file and one +`_fq.dat` file. +Far-field probes are always frequency-domain results, despite not using the +`_fq` suffix. + +## Volumetric Output + +`movie` probes write their files in a directory whose basename is +`P = R_N_F_B`. +The visible files are identical for builds with and without MPI; MPI transfers +or collectively writes the rank-local data into the canonical files. + +| Probe type | Without MPI | With MPI | +| --- | --- | --- | +| `movie`, time domain | `P/P.bin`, `P/P.xdmf`, `P/P.h5`, `P/P_geometry.xdmf`, and `P/P_geometry.h5` | The same five files. | +| frequency slice (`movie`, frequency domain) | `P/P.xdmf`, `P/P.h5`, `P/P_geometry.xdmf`, and `P/P_geometry.h5` | The same four files. | + +Time-domain movie `.bin` files contain the sampled values. +Frequency slices do not create a `.bin` file. +They create field data in `P.xdmf` and `P.h5`, plus the geometry companion in +`P_geometry.xdmf` and `P_geometry.h5`. +The `.xdmf` files describe the corresponding data and geometry stored in HDF5. + +## Geometry Map Output + +The `-mapvtk` option creates a geometry map rather than a JSON +probe. +Without MPI, it writes one `.vtu` file. +With MPI, it writes one `.vtu` piece for every participating rank and a +root-level `.pvtu` descriptor that references all pieces. +Geometry maps do not publish text sidecars. + +Probe outputs do not create JSON descriptors or a run output manifest. +Binary values retain their established byte order, numeric representation, +record size, component order, and complex-value convention. diff --git a/doc/testing.md b/doc/testing.md new file mode 100644 index 000000000..7efdc3771 --- /dev/null +++ b/doc/testing.md @@ -0,0 +1,199 @@ +# Testing + +Run all commands in this document from the repository root. + +## Prerequisites + +Initialise the repository submodules before configuring the project: + +```shell +git submodule update --init --recursive +``` + +The native tests require a working CMake, C/C++ compiler, and Fortran compiler. +The Python tests require Python 3 and the dependencies in `requirements.txt`. +MPI tests additionally require an MPI implementation such as Open MPI. + +For platform-specific compiler and library setup, see +[`development.md`](development.md). + +## Build + +Configure and build a Release tree with native tests enabled: + +```shell +cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DSEMBA_FDTD_ENABLE_TEST=ON +cmake --build build -j +``` + +The `build/` directory is intentional. Some Python end-to-end tests look for +the solver at `build/bin/semba-fdtd`. + +For a Debug build, change `Release` to `Debug`. The project also provides CMake +presets, for example: + +```shell +cmake --preset dbg +cmake --build --preset dbg +``` + +Preset builds use directories such as `build-dbg/`. + +## Native Tests + +Run all tests registered with CTest: + +```shell +ctest --test-dir build --output-on-failure +``` + +List the registered tests without running them: + +```shell +ctest --test-dir build -N +``` + +The main native test executable can also be run directly: + +```shell +./build/bin/fdtd_tests +``` + +### Running a Specific Native Test + +CTest test names can be listed before running them: + +```shell +ctest --test-dir build -N +``` + +Run one exact CTest test with `-R`: + +```shell +ctest --test-dir build -R '^fdtd_unit$' --output-on-failure +``` + +The expression passed to `-R` is a regular expression. For example, run all +tests whose names contain `output`: + +```shell +ctest --test-dir build -R output --output-on-failure +``` + +Add `-V` to show the complete command and output for a test: + +```shell +ctest --test-dir build -V -R '^fdtd_unit$' +``` + +The native executable uses GoogleTest. List its individual test cases with: + +```shell +./build/bin/fdtd_tests --gtest_list_tests +``` + +Run an individual GoogleTest case with `--gtest_filter`: + +```shell +./build/bin/fdtd_tests --gtest_filter='TestSuiteName.TestName' +``` + +Wildcards can be used in a GoogleTest filter: + +```shell +./build/bin/fdtd_tests --gtest_filter='*Conformal*' +``` + +## Python Tests + +Create a virtual environment and install the test dependencies: + +```shell +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install -r requirements.txt +``` + +Run the complete Python test suite: + +```shell +pytest test/ --durations=20 +``` + +Equivalent invocation: + +```shell +python3 -m pytest test/ +``` + +Run a specific file or test: + +```shell +pytest test/pyWrapper/test_integration.py +pytest test/pyWrapper/test_integration.py -k test_name +``` + +Pytest also supports selecting a test by its full node ID. The node ID has the +form `path/to/test_file.py::test_name`, or includes a class name for class +based tests: + +```shell +pytest test/pyWrapper/test_integration.py::test_name +pytest test/pyWrapper/test_integration.py::TestClass::test_name +``` + +Use `--collect-only` to discover available test names without running them: + +```shell +pytest test/pyWrapper/test_integration.py --collect-only -q +``` + +The `-k` expression can match part of a test name. Use `-vv` for more detail +and `-x` to stop after the first failure: + +```shell +pytest test/ -k 'conformal and not mpi' -vv +pytest test/ -k test_name -x +``` + +## Test Markers + +Use markers to select tests for optional features: + +```shell +pytest test/ -m mtln +pytest test/ -m hdf +pytest test/ -m mpi +pytest test/ -m "not mpi" +``` + +Available markers are listed in [`../pytest.ini`](../pytest.ini), including +`mtln`, `hdf`, `mpi`, `spice`, `vtk`, `conformal`, `wires`, and `movie`. + +## MPI Tests + +Configure and build with MPI enabled: + +```shell +cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DSEMBA_FDTD_ENABLE_TEST=ON \ + -DSEMBA_FDTD_ENABLE_MPI=ON +cmake --build build -j +``` + +Run the CTest suite, including MPI tests: + +```shell +ctest --test-dir build --output-on-failure +``` + +Run the Python MPI tests separately: + +```shell +pytest test/ -m mpi +``` + +MPI tests require `mpirun` or `mpiexec` to be available in `PATH`. diff --git a/docker-compose.yml b/docker-compose.yml index b9fd2ffa9..7620554b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,43 +1,95 @@ services: - - # Run a simulation: docker compose run --rm solver path/to/case.fdtd.json - solver: + # Lightweight image for compiling and testing the solver. + quality: build: context: . - target: runtime + target: quality volumes: - - ./simulations:/work - working_dir: /work + # Work directly against the checked-out source tree. + - type: bind + source: . + target: /home/developer/workspaces/fdtd + working_dir: /home/developer/workspaces/fdtd + tty: true - # Debug a simulation with gdbserver (connect from VSCode with launch.json) - # Usage: docker compose run --rm -p 2345:2345 debug case.fdtd.json - debug: + # Intel oneAPI compiler and Intel MPI environment for manual quality builds. + intel-quality: build: context: . - target: builder - args: - BUILD_TYPE: Debug - ENABLE_MTLN: "ON" + target: intel-quality + working_dir: /home/developer/workspaces/fdtd + stdin_open: true + tty: true + + # Intel oneAPI development environment. It intentionally does not inherit + # from the GNU-based dev service or image. + intel-dev: + build: + context: . + target: intel-dev volumes: - - ./simulations:/work - ports: - - "2345:2345" - working_dir: /work - entrypoint: ["gdbserver", ":2345", "/src/build/bin/semba-fdtd", "-i"] + - type: bind + source: . + target: /home/developer/workspaces/fdtd + - type: bind + source: ${HOME}/.gitconfig + target: /home/developer/.gitconfig + - type: bind + source: ${HOME}/.ssh + target: /home/developer/.ssh + read_only: true + - type: bind + source: ${HOME}/.config/gh + target: /home/developer/.config/gh + - type: bind + source: ${HOME}/.config/opencode + target: /home/developer/.config/opencode + - type: bind + source: ${HOME}/.agents + target: /home/developer/.agents + read_only: true + - type: bind + source: ${HOME}/.local/share/opencode + target: /home/developer/.local/share/opencode + working_dir: /home/developer/workspaces/fdtd + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + stdin_open: true + tty: true - # Build the project and run all tests - test: + # Interactive development image with source-control and OpenCode tooling. + dev: build: context: . - target: builder - args: - BUILD_TYPE: Release - ENABLE_MPI: "OFF" - ENABLE_MTLN: "ON" - environment: - SEMBA_FDTD_ENABLE_MPI: "OFF" - SEMBA_FDTD_ENABLE_MTLN: "ON" - SEMBA_FDTD_ENABLE_HDF: "ON" - working_dir: /src - command: > - sh -c "build/bin/fdtd_tests && python3 -m pytest test/ --durations=20" + target: dev + volumes: + - type: bind + source: . + target: /home/developer/workspaces/fdtd + # Use the host Git identity and SSH keys for authenticated Git operations. + - type: bind + source: ${HOME}/.gitconfig + target: /home/developer/.gitconfig + - type: bind + source: ${HOME}/.ssh + target: /home/developer/.ssh + read_only: true + - type: bind + source: ${HOME}/.config/gh + target: /home/developer/.config/gh + # Share local OpenCode configuration and agent resources with the container. + - type: bind + source: ${HOME}/.config/opencode + target: /home/developer/.config/opencode + - type: bind + source: ${HOME}/.agents + target: /home/developer/.agents + read_only: true + # Keep OpenCode's local data available between disposable containers. + - type: bind + source: ${HOME}/.local/share/opencode + target: /home/developer/.local/share/opencode + working_dir: /home/developer/workspaces/fdtd + tty: true diff --git a/docker/intel-quality-entrypoint.sh b/docker/intel-quality-entrypoint.sh new file mode 100644 index 000000000..83e03aeba --- /dev/null +++ b/docker/intel-quality-entrypoint.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +# Initialise compiler and Intel MPI paths for every interactive container. +source /opt/intel/oneapi/setvars.sh --force >/dev/null + +export HDF5_ROOT="${HDF5_ROOT:-/home/developer/workspaces/fdtd/precompiled_libraries/linux-intel/hdf5}" +export LD_LIBRARY_PATH="${HDF5_ROOT}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +exec "$@" diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 2e8751b23..781a6f578 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -25,4 +25,6 @@ if (SEMBA_FDTD_ENABLE_MTLN) target_compile_options(ngspice-spicelib PRIVATE "$<$,$,$>>:-Wno-invalid-specialization>") endif() # set(NGSPICE_LIB ngspice) -endif() \ No newline at end of file +endif() + +add_subdirectory("xdmf-hdf5/") diff --git a/external/ngspice b/external/ngspice index d4a8166e2..909c2731f 160000 --- a/external/ngspice +++ b/external/ngspice @@ -1 +1 @@ -Subproject commit d4a8166e2b6f8dccd12775fe00958eb4d32685e3 +Subproject commit 909c2731f3bb67742bdeeeb0c6e7d296ac76fd8b diff --git a/external/xdmf-hdf5/CMakeLists.txt b/external/xdmf-hdf5/CMakeLists.txt new file mode 100644 index 000000000..c89468f4c --- /dev/null +++ b/external/xdmf-hdf5/CMakeLists.txt @@ -0,0 +1,126 @@ +cmake_minimum_required(VERSION 3.15) + +project( + xdmf-hdf5 + VERSION 0.1.0 + LANGUAGES Fortran +) + +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + +if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) + set(_xdmf_hdf5_test_default ON) +else() + set(_xdmf_hdf5_test_default OFF) +endif() + +option( + XDMF_HDF5_BUILD_TESTING + "Build the XDMF/HDF5 conformance suite" + ${_xdmf_hdf5_test_default} +) + +find_package(HDF5 REQUIRED COMPONENTS Fortran) + +option( + XDMF_HDF5_ENABLE_MPI + "Build the external writer MPI API" + OFF +) + +set(XDMF_HDF5_MPI_ENABLED FALSE) +if(XDMF_HDF5_ENABLE_MPI) + find_package(MPI REQUIRED COMPONENTS Fortran) + set(XDMF_HDF5_MPI_ENABLED TRUE) +endif() + +set(XDMF_HDF5_PARALLEL_AVAILABLE FALSE) +if(XDMF_HDF5_MPI_ENABLED AND HDF5_IS_PARALLEL) + include("${CMAKE_CURRENT_LIST_DIR}/cmake/CheckParallelHdf5Fortran.cmake") + xdmf_check_parallel_hdf5_fortran(XDMF_HDF5_PARALLEL_AVAILABLE) +endif() + +add_library(xdmf_hdf5 + src/xdmf_model.F90 + src/xdmf_hdf5_backend.F90 + src/xdmf_xml.F90 + src/xdmf_hdf5.F90 +) +add_library(XDMF::HDF5 ALIAS xdmf_hdf5) + +set_target_properties(xdmf_hdf5 PROPERTIES + EXPORT_NAME HDF5 + Fortran_MODULE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/modules" + Fortran_STANDARD 2018 + Fortran_STANDARD_REQUIRED YES + Fortran_EXTENSIONS OFF +) + +target_include_directories(xdmf_hdf5 + PUBLIC + $ + $ +) +if(NOT TARGET HDF5::HDF5) + add_library(HDF5::HDF5 INTERFACE IMPORTED) + set_target_properties(HDF5::HDF5 PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${HDF5_Fortran_INCLUDE_DIRS}" + INTERFACE_LINK_LIBRARIES "${HDF5_Fortran_LIBRARIES}" + ) +endif() +target_link_libraries(xdmf_hdf5 PUBLIC HDF5::HDF5) +if(XDMF_HDF5_MPI_ENABLED) + target_link_libraries(xdmf_hdf5 PUBLIC MPI::MPI_Fortran) + target_compile_definitions(xdmf_hdf5 PRIVATE XDMF_HDF5_WITH_MPI) +endif() +if(XDMF_HDF5_PARALLEL_AVAILABLE) + target_compile_definitions(xdmf_hdf5 PRIVATE XDMF_HDF5_WITH_PARALLEL_HDF5) + target_compile_definitions(xdmf_hdf5 PUBLIC XDMF_HDF5_PARALLEL_AVAILABLE) +endif() + +if(XDMF_HDF5_BUILD_TESTING) + enable_testing() + add_subdirectory(test) +endif() + +install( + TARGETS xdmf_hdf5 + EXPORT XDMFHdf5Targets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) +install( + DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/modules/" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/xdmf-hdf5" + FILES_MATCHING PATTERN "*.mod" +) + +configure_package_config_file( + cmake/XDMFHdf5Config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/XDMFHdf5Config.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/XDMFHdf5" +) +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/XDMFHdf5ConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + +install( + EXPORT XDMFHdf5Targets + FILE XDMFHdf5Targets.cmake + NAMESPACE XDMF:: + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/XDMFHdf5" +) +install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/XDMFHdf5Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/XDMFHdf5ConfigVersion.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/XDMFHdf5" +) +install( + FILES README.md LICENSE + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/doc/xdmf-hdf5" +) diff --git a/external/xdmf-hdf5/LICENSE b/external/xdmf-hdf5/LICENSE new file mode 100644 index 000000000..05d4874bc --- /dev/null +++ b/external/xdmf-hdf5/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Elemwave + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/xdmf-hdf5/README.md b/external/xdmf-hdf5/README.md new file mode 100644 index 000000000..199e881b1 --- /dev/null +++ b/external/xdmf-hdf5/README.md @@ -0,0 +1,181 @@ +# XDMF/HDF5 + +`xdmf-hdf5` is a solver-independent Fortran library for producing +XDMF 3 metadata and its associated HDF5 heavy data. +It is developed in-tree initially, but has its own build, tests, package +metadata, and public API so it can later move to a separate repository. + +## Scope + +Version 0.1 provides: + +- Uniform, rectilinear, curvilinear, unstructured, and mixed grids. +- Linear and quadratic XDMF topologies. +- Static, temporal, frequency, and generic parameter collections. +- Scalar, vector, tensor, matrix, and identifier attributes. +- Node, edge, face, cell, and grid-centred data. +- `real32`, `real64`, `int32`, and `int64` heavy data. +- Explicit real/imaginary or magnitude/phase attributes for complex data. + +The library does not depend on solver types, OpenMP, or SMBJSON. +HDF5 identifiers and XML serialization are private implementation details. + +When CMake discovers an MPI-capable HDF5 Fortran installation, the library also +supports collective scalar-series hyperslab writes. +Set `XDMF_HDF5_ENABLE_MPI=ON` to enable the MPI API. +Collective HDF5 support is detected automatically in either build mode. + +## Standalone Build + +```sh +cmake -S . -B build -DXDMF_HDF5_BUILD_TESTING=ON +cmake --build build -j +ctest --test-dir build --output-on-failure +``` + +## Example Generator + +With `XDMF_HDF5_BUILD_TESTING=ON`, the build produces +`xdmf_hdf5_generate_cases` in the build's binary directory. +It generates the XDMF/HDF5 conformance examples: + +```sh +./build/bin/xdmf_hdf5_generate_cases [options] +``` + +The output directory is created when necessary. +The generator preserves existing files by default and rejects the invocation +before writing when one of its output pairs already exists. +Use `--replace` to explicitly replace the generator's named output pairs: + +```sh +./build/bin/xdmf_hdf5_generate_cases ./generated --replace +``` + +Available options are: + +```text +--examples Generate only the committed examples. +--help Print command usage and exit. +--replace Replace generated files that already exist. +``` + +Consumers link the exported target: + +```cmake +find_package(XDMFHdf5 CONFIG REQUIRED) +target_link_libraries(my_writer PRIVATE XDMF::HDF5) +``` + +## API Example + +The writer owns the pair and every HDF5 resource. +All operations return an `xdmf_status_t`; the library never prints or stops the +calling application. + +```fortran +use, intrinsic :: iso_fortran_env, only: int64, real64 +use xdmf_hdf5_m + +type(xdmf_writer_t) :: writer +type(xdmf_options_t) :: options +type(xdmf_status_t) :: status +type(xdmf_grid_id_t) :: grid +type(xdmf_attribute_id_t) :: pressure +real(real64) :: values(24) + +options%overwrite = .true. +options%series_kind = XDMF_SERIES_TIME + +call writer%create('result', options, status) +call writer%define_uniform_grid('volume', [2_int64, 3_int64, 4_int64], & + [0.0_real64, 0.0_real64, 0.0_real64], & + [1.0_real64, 1.0_real64, 1.0_real64], grid, status) +call writer%define_attribute(grid, 'pressure', XDMF_CENTER_NODE, & + XDMF_ATTRIBUTE_SCALAR, XDMF_NUMERIC_REAL64, .true., pressure, status) + +call writer%begin_step(0.0_real64, status) +call writer%write_attribute(pressure, values, status) +call writer%end_step(status) +call writer%close(status) +``` + +Applications should check `status%is_error()` after each operation. +`status%message()` contains contextual failure information. +`XDMF_ERROR_CONSISTENCY` means an HDF5 rollback or resource close could not be +confirmed; the writer must then be closed and not reused. + +`xdmf_writer_t` is a unique resource owner. +Do not assign or copy an open writer; pass it with `intent(inout)` and close it +explicitly before it leaves scope. + +## Collective HDF5 + +Collective output is available only when the library was built against parallel +HDF5 with a compatible MPI Fortran implementation. +All ranks must call writer creation, definitions, step operations, flush, and +close in the same order. +Only `root_rank` publishes the XDMF document. + +```fortran +options%overwrite = .true. +options%series_kind = XDMF_SERIES_TIME +options%collective_io = .true. +options%communicator = MPI_COMM_WORLD +options%root_rank = 0 + +call writer%create('result', options, status) +! Define the same grid and scalar series attribute on every rank. +call writer%begin_step(time, status) +call writer%write_attribute_hyperslab(field, local_values, & + local_offset, local_shape, status) +call writer%end_step(status) +``` + +`local_offset` is zero-based and `local_shape` is in Fortran I/J/K order. +They select a disjoint portion of the globally defined scalar attribute. +Ranks with no cells pass zero for every `local_shape` entry and an empty value +array; they still participate in the collective call. +Compression is intentionally unavailable in collective mode. + +## Data Conventions + +The Fortran API accepts connectivity with one-based node indices. +The writer validates and converts it to the zero-based indexing required by +XDMF 3. + +Attribute values are supplied as a rank-one array in Fortran column-major +order. +The expected shape is fixed when the attribute is defined and is validated on +every write. + +Structured dimensions are supplied in I/J/K order. +The generated HDF5 and XDMF metadata expose dimensions in K/J/I order, with +the series axis first and the component axis last where present. + +## Reader Compatibility + +The conformance suite checks all generated metadata and heavy data directly. +When `pvpython` is available, it also loads representative uniform, +curvilinear, unstructured, mixed, and temporal outputs through ParaView's +XDMF reader. + +Ready-to-open examples of those outputs are committed under +[`examples/generated`](examples/generated/README.md). +Keep each `.xdmf` file beside its corresponding `.h5` file when opening it in +ParaView. + +Some ParaView releases still use the legacy XDMF2 reader internally. +That reader ignores edge- and face-centred attributes and can fail on valid +higher-rank `Matrix` attributes. +Scalar, vector, tensor, topology, and temporal data remain independently +validated even when a reader lacks support for one XDMF feature. + +## Schema Stability + +Every HDF5 file carries `schema_name` and `schema_version` root attributes. +Version 1.0 stores immutable grid data under `/grids`, attribute values under +`/attributes`, and series coordinates under `/series/values`. +Display names are kept in XDMF metadata and never become HDF5 paths. + +Backward-incompatible storage changes require a new major schema version. diff --git a/external/xdmf-hdf5/cmake/CheckParallelHdf5Fortran.cmake b/external/xdmf-hdf5/cmake/CheckParallelHdf5Fortran.cmake new file mode 100644 index 000000000..68d0511e8 --- /dev/null +++ b/external/xdmf-hdf5/cmake/CheckParallelHdf5Fortran.cmake @@ -0,0 +1,40 @@ +include(CMakePushCheckState) +include(CheckFortranSourceCompiles) + +function(xdmf_check_parallel_hdf5_fortran result) + set(${result} FALSE PARENT_SCOPE) + + if(NOT HDF5_IS_PARALLEL) + return() + endif() + + cmake_push_check_state(RESET) + set(CMAKE_REQUIRED_INCLUDES + ${HDF5_Fortran_INCLUDE_DIRS} + ${MPI_Fortran_INCLUDE_DIRS} + ) + set(CMAKE_REQUIRED_LIBRARIES + ${HDF5_Fortran_LIBRARIES} + ${HDF5_LIBRARIES} + MPI::MPI_Fortran + ) + check_fortran_source_compiles([=[ +program parallel_hdf5_fortran_probe + use hdf5 + use mpi + implicit none + integer :: error + integer(HID_T) :: file_access_plist, transfer_plist + + call h5pcreate_f(H5P_FILE_ACCESS_F, file_access_plist, error) + call h5pset_fapl_mpio_f(file_access_plist, MPI_COMM_WORLD, MPI_INFO_NULL, error) + call h5pcreate_f(H5P_DATASET_XFER_F, transfer_plist, error) + call h5pset_dxpl_mpio_f(transfer_plist, H5FD_MPIO_COLLECTIVE_F, error) +end program parallel_hdf5_fortran_probe +]=] XDMF_PARALLEL_HDF5_FORTRAN_PROBE_WORKS SRC_EXT F90) + cmake_pop_check_state() + + if(XDMF_PARALLEL_HDF5_FORTRAN_PROBE_WORKS) + set(${result} TRUE PARENT_SCOPE) + endif() +endfunction() diff --git a/external/xdmf-hdf5/cmake/XDMFHdf5Config.cmake.in b/external/xdmf-hdf5/cmake/XDMFHdf5Config.cmake.in new file mode 100644 index 000000000..4e897d9d4 --- /dev/null +++ b/external/xdmf-hdf5/cmake/XDMFHdf5Config.cmake.in @@ -0,0 +1,20 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(HDF5 REQUIRED COMPONENTS Fortran) + +if(NOT TARGET HDF5::HDF5) + add_library(HDF5::HDF5 INTERFACE IMPORTED) + set_target_properties(HDF5::HDF5 PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${HDF5_Fortran_INCLUDE_DIRS}" + INTERFACE_LINK_LIBRARIES "${HDF5_Fortran_LIBRARIES}" + ) +endif() + +if(@XDMF_HDF5_MPI_ENABLED@) + find_dependency(MPI REQUIRED COMPONENTS Fortran) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/XDMFHdf5Targets.cmake") + +check_required_components(XDMFHdf5) diff --git a/external/xdmf-hdf5/examples/CMakeLists.txt b/external/xdmf-hdf5/examples/CMakeLists.txt new file mode 100644 index 000000000..679854fbf --- /dev/null +++ b/external/xdmf-hdf5/examples/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.15) + +project(xdmf-hdf5-example LANGUAGES Fortran) + +find_package(XDMFHdf5 CONFIG REQUIRED) + +add_executable(xdmf-hdf5-example basic_writer.F90) +target_link_libraries(xdmf-hdf5-example PRIVATE XDMF::HDF5) +set_target_properties(xdmf-hdf5-example PROPERTIES + Fortran_STANDARD 2018 + Fortran_STANDARD_REQUIRED YES + Fortran_EXTENSIONS OFF +) diff --git a/external/xdmf-hdf5/examples/basic_writer.F90 b/external/xdmf-hdf5/examples/basic_writer.F90 new file mode 100644 index 000000000..a42b7e464 --- /dev/null +++ b/external/xdmf-hdf5/examples/basic_writer.F90 @@ -0,0 +1,52 @@ +program basic_writer + use, intrinsic :: iso_fortran_env, only: int64, real64 + use xdmf_hdf5_m + + implicit none + + type(xdmf_writer_t) :: writer + type(xdmf_options_t) :: options + type(xdmf_status_t) :: status + type(xdmf_grid_id_t) :: grid + type(xdmf_attribute_id_t) :: pressure + real(real64) :: values(24) + integer :: index + + options%overwrite = .true. + options%series_kind = XDMF_SERIES_TIME + call writer%create('example', options, status) + call check(status) + + call writer%define_uniform_grid('volume', & + [2_int64, 3_int64, 4_int64], & + [0.0_real64, 0.0_real64, 0.0_real64], & + [1.0_real64, 1.0_real64, 1.0_real64], grid, status) + call check(status) + call writer%define_attribute(grid, 'pressure', XDMF_CENTER_NODE, & + XDMF_ATTRIBUTE_SCALAR, XDMF_NUMERIC_REAL64, .true., pressure, status) + call check(status) + + do index = 1, size(values) + values(index) = real(index, real64) + end do + call writer%begin_step(0.0_real64, status) + call check(status) + call writer%write_attribute(pressure, values, status) + call check(status) + call writer%end_step(status) + call check(status) + call writer%close(status) + call check(status) + +contains + + subroutine check(result) + type(xdmf_status_t), intent(in) :: result + + if (result%is_error()) then + write(*, '(A)') result%message() + error stop 1 + end if + end subroutine check + +end program basic_writer diff --git a/external/xdmf-hdf5/examples/generated/README.md b/external/xdmf-hdf5/examples/generated/README.md new file mode 100644 index 000000000..d9f21fce1 --- /dev/null +++ b/external/xdmf-hdf5/examples/generated/README.md @@ -0,0 +1,170 @@ +# Generated XDMF/HDF5 Examples + +This directory contains representative output pairs generated by the +XDMF/HDF5 library and checked by its conformance tests. +Open an `.xdmf` file, not its `.h5` companion, and keep both files in the same +directory. + +## Start With Volume Rendering + +Use `volume.xdmf` for a warning-free introduction to volumetric data: + +1. Select **File > Open**, choose `volume.xdmf`, and select the XDMF reader if + ParaView asks which reader to use. +2. Click **Apply** in the Properties panel. +3. Set **Representation** to **Volume**. +4. Set **Coloring** to `electric-field-magnitude` and click **Rescale to Data + Range**. +5. Open the Color Map Editor and adjust **Scalar Opacity** so low values become + transparent. + +The field contains two smooth peaks. +Add a **Slice** filter to inspect its interior or a **Contour** filter with an +isovalue around `0.2` to display the peak regions as surfaces. + +## Explore Each Case + +### `volume.xdmf` + +This is the recommended volume-rendering example. +It is a static `21 x 21 x 21` uniform grid with the node-centred scalar +`electric-field-magnitude`. +Use **Volume**, **Slice**, and **Contour** representations. + +### `time-series.xdmf` + +This is the recommended animation example. +It contains a synthetic y-polarised electromagnetic pulse moving in the +x +direction through a `25 x 25 x 25` uniform grid over 20 normalized time steps. +The pulse is analytic demonstration data, not the result of an FDTD +simulation. + +For a scalar animation: + +1. Follow the temporary workaround below to create a `ResampleToImage` result. +2. Colour that result by `electric-field-magnitude` and choose **Volume**. +3. Click **Rescale to Data Range**, then edit the scalar opacity transfer + function to hide values near zero. +4. Set the animation mode to **Snap To TimeSteps** and press **Play**. +5. Apply a **Slice** normal to the z-axis for the clearest view of the carrier + oscillation moving through the Gaussian envelope. + +For a vector animation, apply **Glyph**, choose `electric-field` for +**Orientation Array**, and use its magnitude for **Scale Array**. +Reduce the glyph density before playing the animation; drawing all 15,625 +vectors at every frame is usually unnecessary. + +#### Temporary Volume-Rendering Workaround + +The current `time-series.xdmf` wraps every timestep in a spatial collection. +Some ParaView versions consequently select the synthetic `vtkCompositeIndex` +array when switching directly to volume rendering and report errors such as: + +```text +No scalars named "vtkCompositeIndex" found on input. +Could not find the requested vtkDataArray. +``` + +Until single-grid temporal output is flattened, use these steps: + +1. Start ParaView and select **File > Open**. +2. Open `time-series.xdmf`. + Do not open `time-series.h5` directly, and keep both files in this + directory. +3. If ParaView asks which reader to use, select **XDMF Reader**. +4. In the **Properties** panel, enable `electric-field-magnitude` under + **Point Arrays**. + Optionally enable `electric-field` as well, then click **Apply**. +5. In the **Pipeline Browser**, select the `time-series.xdmf` source. + If the Pipeline Browser or Properties panel is hidden, enable it under + **View > Panels**. +6. Select **Filters > Alphabetical > Extract Block**. +7. Select the new `ExtractBlock` entry in the Pipeline Browser. + In its **Properties** panel, expand the block tree, select + `travelling-wave-pulse`, and click **Apply**. +8. Hide the original `time-series.xdmf` source by clicking its eye icon in the + Pipeline Browser. +9. Select the `ExtractBlock` result and choose + **Filters > Alphabetical > Resample To Image**. +10. Select the new `ResampleToImage` entry, set **Sampling Dimensions** to + `25`, `25`, `25`, and click **Apply**. +11. Hide the `ExtractBlock` result with its eye icon so that only + `ResampleToImage` remains visible. +12. With `ResampleToImage` selected, open the toolbar colour dropdown, which + initially usually says **Solid Color**. + Under **Point Data**, select `electric-field-magnitude`. + Do not select `vtkCompositeIndex`. +13. Open the representation dropdown, which usually says **Surface**, and + select **Volume**. +14. Open the Color Map Editor and click **Rescale to Data Range**. + Edit **Scalar Opacity** so values near zero are transparent and higher + values remain visible. +15. Use the animation controls at the top of ParaView to play the series. + For discrete frames, open **View > Animation View** and set **Play Mode** + to **Snap To TimeSteps**. + +If **Volume** is unavailable, verify that `ResampleToImage`, rather than the +original source or `ExtractBlock`, is selected. +If the `vtkCompositeIndex` error persists, verify that the colour array is +`electric-field-magnitude` and that only `ResampleToImage` is visible. +Use the static `volume.xdmf` example to test volume rendering without these +extraction steps. + +### `uniform.xdmf` + +This is a two-dimensional uniform grid with the node-centred scalar +`temperature`. +Use **Surface** or **Surface With Edges**, colour by `temperature`, and apply +**Plot Over Line** to inspect interpolation across the grid. +It is not a volumetric dataset. + +### `curvilinear.xdmf` + +This is a two-dimensional curved surface without field attributes. +Use **Surface With Edges** or **Wireframe** to inspect its geometry. +It is not intended for scalar colouring or volume rendering. + +### `unstructured.xdmf` + +This is a conformance collection containing one example of every supported +unstructured topology, including high-order elements. +Use **Surface With Edges** and the multiblock controls to inspect individual +topologies. +This is not a visualisation showcase, and some ParaView versions emit warnings +for high-order topology names that their legacy XDMF reader does not support. + +### `mixed.xdmf` + +This is a conformance case combining vertices, lines, surfaces, a tetrahedron, +a polyhedron, and a high-order triangle in one mixed topology stream. +Use **Surface With Edges** to inspect what the installed reader supports. +Do not use it as a volume-rendering example. +Warnings about unsupported mixed or polyhedral cells can be reader limitations +rather than malformed XDMF/HDF5 data. + +## ParaView Warnings + +Warnings are expected only for the topology-focused `unstructured` and `mixed` +cases on ParaView releases that use the legacy XDMF2 reader internally. +The `volume`, `time-series`, `uniform`, and `curvilinear` cases are exercised +through ParaView in the automated test suite and should load without data +errors. + +If `volume.xdmf` or `time-series.xdmf` produces warnings: + +1. Confirm that the matching `.h5` file is beside the `.xdmf` file. +2. Open the `.xdmf` file rather than importing the `.h5` file directly. +3. Remove the old reader from the Pipeline Browser and reopen the file after + regenerating examples, because ParaView can cache HDF5 metadata. +4. Record the ParaView version and the first warning in **View > Output + Messages** when reporting the issue. + +To refresh the committed pairs after changing the writer, configure with tests +enabled and run: + +```sh +cmake --build build --target xdmf-hdf5-refresh-examples +``` + +Normal test runs generate temporary outputs in the build tree and do not +modify these committed files. diff --git a/external/xdmf-hdf5/examples/generated/curvilinear.h5 b/external/xdmf-hdf5/examples/generated/curvilinear.h5 new file mode 100644 index 000000000..2ca434ea5 Binary files /dev/null and b/external/xdmf-hdf5/examples/generated/curvilinear.h5 differ diff --git a/external/xdmf-hdf5/examples/generated/curvilinear.xdmf b/external/xdmf-hdf5/examples/generated/curvilinear.xdmf new file mode 100644 index 000000000..f1bef93b8 --- /dev/null +++ b/external/xdmf-hdf5/examples/generated/curvilinear.xdmf @@ -0,0 +1,14 @@ + + + + + + + + + curvilinear.h5:/grids/g0001/points + + + + + diff --git a/external/xdmf-hdf5/examples/generated/mixed.h5 b/external/xdmf-hdf5/examples/generated/mixed.h5 new file mode 100644 index 000000000..cf16120f9 Binary files /dev/null and b/external/xdmf-hdf5/examples/generated/mixed.h5 differ diff --git a/external/xdmf-hdf5/examples/generated/mixed.xdmf b/external/xdmf-hdf5/examples/generated/mixed.xdmf new file mode 100644 index 000000000..28c12e370 --- /dev/null +++ b/external/xdmf-hdf5/examples/generated/mixed.xdmf @@ -0,0 +1,18 @@ + + + + + + + + mixed.h5:/grids/g0001/connectivity + + + + + mixed.h5:/grids/g0001/points + + + + + diff --git a/external/xdmf-hdf5/examples/generated/time-series.h5 b/external/xdmf-hdf5/examples/generated/time-series.h5 new file mode 100644 index 000000000..0c308b1bf Binary files /dev/null and b/external/xdmf-hdf5/examples/generated/time-series.h5 differ diff --git a/external/xdmf-hdf5/examples/generated/time-series.xdmf b/external/xdmf-hdf5/examples/generated/time-series.xdmf new file mode 100644 index 000000000..42b78da04 --- /dev/null +++ b/external/xdmf-hdf5/examples/generated/time-series.xdmf @@ -0,0 +1,768 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/external/xdmf-hdf5/examples/generated/uniform.h5 b/external/xdmf-hdf5/examples/generated/uniform.h5 new file mode 100644 index 000000000..d4efe0c0b Binary files /dev/null and b/external/xdmf-hdf5/examples/generated/uniform.h5 differ diff --git a/external/xdmf-hdf5/examples/generated/uniform.xdmf b/external/xdmf-hdf5/examples/generated/uniform.xdmf new file mode 100644 index 000000000..85e65fbcd --- /dev/null +++ b/external/xdmf-hdf5/examples/generated/uniform.xdmf @@ -0,0 +1,22 @@ + + + + + + + + + uniform.h5:/grids/g0001/origin + + + uniform.h5:/grids/g0001/spacing + + + + + uniform.h5:/attributes/a0001/values + + + + + diff --git a/external/xdmf-hdf5/examples/generated/unstructured.h5 b/external/xdmf-hdf5/examples/generated/unstructured.h5 new file mode 100644 index 000000000..f2ff61a43 Binary files /dev/null and b/external/xdmf-hdf5/examples/generated/unstructured.h5 differ diff --git a/external/xdmf-hdf5/examples/generated/unstructured.xdmf b/external/xdmf-hdf5/examples/generated/unstructured.xdmf new file mode 100644 index 000000000..475aa5a15 --- /dev/null +++ b/external/xdmf-hdf5/examples/generated/unstructured.xdmf @@ -0,0 +1,248 @@ + + + + + + + + + unstructured.h5:/grids/g0001/connectivity + + + + + unstructured.h5:/grids/g0001/points + + + + + + + unstructured.h5:/grids/g0002/connectivity + + + + + unstructured.h5:/grids/g0002/points + + + + + + + unstructured.h5:/grids/g0003/connectivity + + + + + unstructured.h5:/grids/g0003/points + + + + + + + unstructured.h5:/grids/g0004/connectivity + + + + + unstructured.h5:/grids/g0004/points + + + + + + + unstructured.h5:/grids/g0005/connectivity + + + + + unstructured.h5:/grids/g0005/points + + + + + + + unstructured.h5:/grids/g0006/connectivity + + + + + unstructured.h5:/grids/g0006/points + + + + + + + unstructured.h5:/grids/g0007/connectivity + + + + + unstructured.h5:/grids/g0007/points + + + + + + + unstructured.h5:/grids/g0008/connectivity + + + + + unstructured.h5:/grids/g0008/points + + + + + + + unstructured.h5:/grids/g0009/connectivity + + + + + unstructured.h5:/grids/g0009/points + + + + + + + unstructured.h5:/grids/g0010/connectivity + + + + + unstructured.h5:/grids/g0010/points + + + + + + + unstructured.h5:/grids/g0011/connectivity + + + + + unstructured.h5:/grids/g0011/points + + + + + + + unstructured.h5:/grids/g0012/connectivity + + + + + unstructured.h5:/grids/g0012/points + + + + + + + unstructured.h5:/grids/g0013/connectivity + + + + + unstructured.h5:/grids/g0013/points + + + + + + + unstructured.h5:/grids/g0014/connectivity + + + + + unstructured.h5:/grids/g0014/points + + + + + + + unstructured.h5:/grids/g0015/connectivity + + + + + unstructured.h5:/grids/g0015/points + + + + + + + unstructured.h5:/grids/g0016/connectivity + + + + + unstructured.h5:/grids/g0016/points + + + + + + + unstructured.h5:/grids/g0017/connectivity + + + + + unstructured.h5:/grids/g0017/points + + + + + + + unstructured.h5:/grids/g0018/connectivity + + + + + unstructured.h5:/grids/g0018/points + + + + + + + unstructured.h5:/grids/g0019/connectivity + + + + + unstructured.h5:/grids/g0019/points + + + + + + + unstructured.h5:/grids/g0020/connectivity + + + + + unstructured.h5:/grids/g0020/points + + + + + + diff --git a/external/xdmf-hdf5/examples/generated/volume.h5 b/external/xdmf-hdf5/examples/generated/volume.h5 new file mode 100644 index 000000000..84a291ebb Binary files /dev/null and b/external/xdmf-hdf5/examples/generated/volume.h5 differ diff --git a/external/xdmf-hdf5/examples/generated/volume.xdmf b/external/xdmf-hdf5/examples/generated/volume.xdmf new file mode 100644 index 000000000..b4a670a24 --- /dev/null +++ b/external/xdmf-hdf5/examples/generated/volume.xdmf @@ -0,0 +1,22 @@ + + + + + + + + + volume.h5:/grids/g0001/origin + + + volume.h5:/grids/g0001/spacing + + + + + volume.h5:/attributes/a0001/values + + + + + diff --git a/external/xdmf-hdf5/src/xdmf_hdf5.F90 b/external/xdmf-hdf5/src/xdmf_hdf5.F90 new file mode 100644 index 000000000..ca380798e --- /dev/null +++ b/external/xdmf-hdf5/src/xdmf_hdf5.F90 @@ -0,0 +1,2620 @@ +module xdmf_hdf5_m + use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64 +#ifdef XDMF_HDF5_WITH_MPI + use mpi +#endif + use xdmf_model_m, only: xdmf_status_t, xdmf_options_t, & + xdmf_collection_id_t, xdmf_grid_id_t, xdmf_attribute_id_t, & + collection_record_t, grid_record_t, attribute_record_t, & + XDMF_SUCCESS, XDMF_ERROR_ARGUMENT, XDMF_ERROR_STATE, & + XDMF_ERROR_IO, XDMF_ERROR_HDF5, XDMF_ERROR_CONSISTENCY, & + XDMF_SERIES_NONE, XDMF_SERIES_TIME, XDMF_SERIES_FREQUENCY, & + XDMF_SERIES_PARAMETER, XDMF_GEOMETRY_UNIFORM, & + XDMF_GEOMETRY_RECTILINEAR, XDMF_GEOMETRY_CURVILINEAR, & + XDMF_GEOMETRY_UNSTRUCTURED, XDMF_TOPOLOGY_POLYVERTEX, & + XDMF_TOPOLOGY_POLYLINE, XDMF_TOPOLOGY_POLYGON, & + XDMF_TOPOLOGY_TRIANGLE, XDMF_TOPOLOGY_QUADRILATERAL, & + XDMF_TOPOLOGY_TETRAHEDRON, XDMF_TOPOLOGY_PYRAMID, & + XDMF_TOPOLOGY_WEDGE, XDMF_TOPOLOGY_HEXAHEDRON, & + XDMF_TOPOLOGY_POLYHEDRON, XDMF_TOPOLOGY_EDGE_3, & + XDMF_TOPOLOGY_QUADRILATERAL_9, XDMF_TOPOLOGY_TRIANGLE_6, & + XDMF_TOPOLOGY_QUADRILATERAL_8, XDMF_TOPOLOGY_TETRAHEDRON_10, & + XDMF_TOPOLOGY_PYRAMID_13, XDMF_TOPOLOGY_WEDGE_15, & + XDMF_TOPOLOGY_WEDGE_18, XDMF_TOPOLOGY_HEXAHEDRON_20, & + XDMF_TOPOLOGY_HEXAHEDRON_24, XDMF_TOPOLOGY_HEXAHEDRON_27, & + XDMF_TOPOLOGY_MIXED, XDMF_TOPOLOGY_2D_SMESH, & + XDMF_TOPOLOGY_2D_RECTMESH, XDMF_TOPOLOGY_2D_CORECTMESH, & + XDMF_TOPOLOGY_3D_SMESH, XDMF_TOPOLOGY_3D_RECTMESH, & + XDMF_TOPOLOGY_3D_CORECTMESH, XDMF_CENTER_NODE, & + XDMF_CENTER_EDGE, XDMF_CENTER_FACE, XDMF_CENTER_CELL, & + XDMF_CENTER_GRID, XDMF_ATTRIBUTE_SCALAR, XDMF_ATTRIBUTE_VECTOR, & + XDMF_ATTRIBUTE_TENSOR, XDMF_ATTRIBUTE_TENSOR6, & + XDMF_ATTRIBUTE_MATRIX, XDMF_ATTRIBUTE_GLOBAL_ID, & + XDMF_NUMERIC_REAL32, XDMF_NUMERIC_REAL64, XDMF_NUMERIC_INT32, & + XDMF_NUMERIC_INT64, set_status_success, set_status_error, & + make_collection_id, make_grid_id, make_attribute_id, & + collection_id_value, grid_id_value, attribute_id_value, & + collection_id_owner, grid_id_owner, attribute_id_owner, & + topology_name, topology_nodes_per_element, topology_is_supported, & + center_name, attribute_type_name, numeric_type_name, product_int64 + use xdmf_hdf5_backend_m, only: hdf5_file_t, hdf_create_file, & + hdf_file_is_open, hdf_close_file, hdf_flush_file, hdf_create_group, & + hdf_write_dataset, & + hdf_create_series_dataset, hdf_append_series, & + hdf_append_series_hyperslab, hdf_truncate_series + use xdmf_xml_m, only: write_xdmf_document + + implicit none + + private + + public :: XDMF_SUCCESS, XDMF_ERROR_ARGUMENT, XDMF_ERROR_STATE + public :: XDMF_ERROR_IO, XDMF_ERROR_HDF5 + public :: XDMF_ERROR_CONSISTENCY + public :: XDMF_SERIES_NONE, XDMF_SERIES_TIME + public :: XDMF_SERIES_FREQUENCY, XDMF_SERIES_PARAMETER + public :: XDMF_GEOMETRY_UNIFORM, XDMF_GEOMETRY_RECTILINEAR + public :: XDMF_GEOMETRY_CURVILINEAR, XDMF_GEOMETRY_UNSTRUCTURED + public :: XDMF_TOPOLOGY_POLYVERTEX, XDMF_TOPOLOGY_POLYLINE + public :: XDMF_TOPOLOGY_POLYGON, XDMF_TOPOLOGY_TRIANGLE + public :: XDMF_TOPOLOGY_QUADRILATERAL, XDMF_TOPOLOGY_TETRAHEDRON + public :: XDMF_TOPOLOGY_PYRAMID, XDMF_TOPOLOGY_WEDGE + public :: XDMF_TOPOLOGY_HEXAHEDRON, XDMF_TOPOLOGY_POLYHEDRON + public :: XDMF_TOPOLOGY_EDGE_3, XDMF_TOPOLOGY_QUADRILATERAL_9 + public :: XDMF_TOPOLOGY_TRIANGLE_6, XDMF_TOPOLOGY_QUADRILATERAL_8 + public :: XDMF_TOPOLOGY_TETRAHEDRON_10, XDMF_TOPOLOGY_PYRAMID_13 + public :: XDMF_TOPOLOGY_WEDGE_15, XDMF_TOPOLOGY_WEDGE_18 + public :: XDMF_TOPOLOGY_HEXAHEDRON_20, XDMF_TOPOLOGY_HEXAHEDRON_24 + public :: XDMF_TOPOLOGY_HEXAHEDRON_27, XDMF_TOPOLOGY_MIXED + public :: XDMF_TOPOLOGY_2D_SMESH, XDMF_TOPOLOGY_2D_RECTMESH + public :: XDMF_TOPOLOGY_2D_CORECTMESH, XDMF_TOPOLOGY_3D_SMESH + public :: XDMF_TOPOLOGY_3D_RECTMESH, XDMF_TOPOLOGY_3D_CORECTMESH + public :: XDMF_CENTER_NODE, XDMF_CENTER_EDGE, XDMF_CENTER_FACE + public :: XDMF_CENTER_CELL, XDMF_CENTER_GRID + public :: XDMF_ATTRIBUTE_SCALAR, XDMF_ATTRIBUTE_VECTOR + public :: XDMF_ATTRIBUTE_TENSOR, XDMF_ATTRIBUTE_TENSOR6 + public :: XDMF_ATTRIBUTE_MATRIX, XDMF_ATTRIBUTE_GLOBAL_ID + public :: XDMF_NUMERIC_REAL32, XDMF_NUMERIC_REAL64 + public :: XDMF_NUMERIC_INT32, XDMF_NUMERIC_INT64 + public :: xdmf_status_t, xdmf_options_t, xdmf_collection_id_t + public :: xdmf_grid_id_t, xdmf_attribute_id_t + + character(len=*), parameter :: SERIES_VALUES_PATH = '/series/values' + integer(int64), save :: next_writer_token = 1_int64 + + type, public :: xdmf_writer_t + private + type(hdf5_file_t) :: hdf5_file + type(xdmf_options_t) :: options + type(collection_record_t), allocatable :: collections(:) + type(grid_record_t), allocatable :: grids(:) + type(attribute_record_t), allocatable :: attributes(:) + real(real64), allocatable :: series_values(:) + character(len=:), allocatable :: xdmf_path + character(len=:), allocatable :: hdf5_path + character(len=:), allocatable :: hdf5_name + integer :: next_collection_id = 1 + integer :: next_grid_id = 1 + integer :: next_attribute_id = 1 + integer :: committed_steps = 0 + integer :: communicator = 0 + integer :: rank = 0 + integer :: root_rank = 0 + integer(int64) :: owner_token = 0_int64 + real(real64) :: active_step_value = 0.0_real64 + logical :: is_open = .false. + logical :: definitions_locked = .false. + logical :: step_is_active = .false. + logical :: is_poisoned = .false. + logical :: is_collective = .false. + contains + procedure, private :: writer_create_with_options + procedure, private :: writer_create_default + generic, public :: create => writer_create_with_options, & + writer_create_default + procedure, public :: define_collection => writer_define_collection + procedure, private :: writer_define_uniform_grid_r4 + procedure, private :: writer_define_uniform_grid_r8 + generic, public :: define_uniform_grid => & + writer_define_uniform_grid_r4, writer_define_uniform_grid_r8 + procedure, private :: writer_define_rectilinear_grid_2d_r4 + procedure, private :: writer_define_rectilinear_grid_2d_r8 + procedure, private :: writer_define_rectilinear_grid_3d_r4 + procedure, private :: writer_define_rectilinear_grid_3d_r8 + generic, public :: define_rectilinear_grid => & + writer_define_rectilinear_grid_2d_r4, & + writer_define_rectilinear_grid_2d_r8, & + writer_define_rectilinear_grid_3d_r4, & + writer_define_rectilinear_grid_3d_r8 + procedure, private :: writer_define_curvilinear_grid_r4 + procedure, private :: writer_define_curvilinear_grid_r8 + generic, public :: define_curvilinear_grid => & + writer_define_curvilinear_grid_r4, & + writer_define_curvilinear_grid_r8 + procedure, private :: writer_define_unstructured_grid_r4 + procedure, private :: writer_define_unstructured_grid_r8 + generic, public :: define_unstructured_grid => & + writer_define_unstructured_grid_r4, & + writer_define_unstructured_grid_r8 + procedure, private :: writer_define_mixed_grid_r4 + procedure, private :: writer_define_mixed_grid_r8 + generic, public :: define_mixed_grid => writer_define_mixed_grid_r4, & + writer_define_mixed_grid_r8 + procedure, private :: writer_define_attribute_with_series + procedure, private :: writer_define_static_attribute + generic, public :: define_attribute => & + writer_define_attribute_with_series, writer_define_static_attribute + procedure, private :: writer_write_attribute_r4 + procedure, private :: writer_write_attribute_r8 + procedure, private :: writer_write_attribute_i4 + procedure, private :: writer_write_attribute_i8 + generic, public :: write_attribute => writer_write_attribute_r4, & + writer_write_attribute_r8, writer_write_attribute_i4, & + writer_write_attribute_i8 + procedure, private :: writer_write_attribute_hyperslab_r4 + procedure, private :: writer_write_attribute_hyperslab_r8 + procedure, private :: writer_write_attribute_hyperslab_i4 + procedure, private :: writer_write_attribute_hyperslab_i8 + generic, public :: write_attribute_hyperslab => & + writer_write_attribute_hyperslab_r4, & + writer_write_attribute_hyperslab_r8, & + writer_write_attribute_hyperslab_i4, & + writer_write_attribute_hyperslab_i8 + procedure, public :: begin_step => writer_begin_step + procedure, public :: end_step => writer_end_step + procedure, public :: flush => writer_flush + procedure, public :: close => writer_close + final :: writer_finalize + end type xdmf_writer_t + +contains + + subroutine writer_create_with_options(this, path, options, status) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: path + type(xdmf_options_t), intent(in) :: options + type(xdmf_status_t), intent(out) :: status + + call writer_create_impl(this, path, options, status) + end subroutine writer_create_with_options + + subroutine writer_create_default(this, path, status) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: path + type(xdmf_status_t), intent(out) :: status + + type(xdmf_options_t) :: options + + call writer_create_impl(this, path, options, status) + end subroutine writer_create_default + + subroutine writer_create_impl(this, path, options, status) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: path + type(xdmf_options_t), intent(in) :: options + type(xdmf_status_t), intent(out) :: status + + integer(int64) :: scalar_shape(0) + integer :: mpi_error + logical :: xdmf_exists + + call set_status_success(status) + if (this%is_open) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'The XDMF writer is already open') + return + end if + if (len_trim(path) == 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'The output path must not be empty') + return + end if + call validate_options(options, status) + if (status%is_error()) return + + call reset_writer_metadata(this) + this%options = options + this%is_collective = options%collective_io + this%communicator = options%communicator + this%root_rank = options%root_rank +#ifdef XDMF_HDF5_WITH_MPI + if (this%is_collective) then + call MPI_Comm_rank(this%communicator, this%rank, mpi_error) + if (mpi_error /= MPI_SUCCESS) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Could not query the collective-output MPI communicator') + return + end if + end if +#endif + call derive_output_paths(path, this%xdmf_path, this%hdf5_path, & + this%hdf5_name) + if (.not. options%overwrite) then + inquire(file=this%xdmf_path, exist=xdmf_exists) + if (xdmf_exists) then + call set_status_error(status, XDMF_ERROR_IO, & + 'Output file already exists: '//this%xdmf_path) + return + end if + end if + + call hdf_create_file(this%hdf5_file, this%hdf5_path, options, status) + call synchronize_collective_status(this, status, & + 'Collective HDF5 file creation failed') + if (status%is_error()) then + if (hdf_file_is_open(this%hdf5_file)) then + this%is_open = .true. + this%is_poisoned = .true. + end if + return + end if + this%is_open = .true. + + call hdf_create_group(this%hdf5_file, '/grids', status) + if (status%is_error()) then + call close_after_create_failure(this, status) + return + end if + call hdf_create_group(this%hdf5_file, '/attributes', status) + if (status%is_error()) then + call close_after_create_failure(this, status) + return + end if + call hdf_create_group(this%hdf5_file, '/series', status) + if (status%is_error()) then + call close_after_create_failure(this, status) + return + end if + if (options%series_kind /= XDMF_SERIES_NONE) then + call hdf_create_series_dataset(this%hdf5_file, SERIES_VALUES_PATH, & + XDMF_NUMERIC_REAL64, scalar_shape, options, status) + if (status%is_error()) call close_after_create_failure(this, status) + end if + end subroutine writer_create_impl + + subroutine writer_define_collection(this, name, collection_id, status) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + type(xdmf_collection_id_t), intent(out) :: collection_id + type(xdmf_status_t), intent(out) :: status + + type(collection_record_t) :: record + integer :: index + + collection_id = make_collection_id(0) + call check_definition_state(this, name, status) + if (status%is_error()) return + do index = 1, size(this%collections) + if (this%collections(index)%name == trim(name)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Collection names must be unique') + return + end if + end do + + record%id = this%next_collection_id + record%name = trim(name) + this%next_collection_id = this%next_collection_id + 1 + call append_collection(this, record, status) + if (.not. status%is_error()) then + collection_id = make_collection_id(record%id, this%owner_token) + end if + end subroutine writer_define_collection + + subroutine writer_define_uniform_grid_r4(this, name, dimensions, origin, & + spacing, grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer(int64), intent(in) :: dimensions(:) + real(real32), intent(in) :: origin(:), spacing(:) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + + grid_id = make_grid_id(0) + call prepare_uniform_grid(this, name, dimensions, origin_size=size(origin), & + spacing_size=size(spacing), record=record, status=status, & + collection_id=collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL32 + + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%origin_path, origin, & + [int(record%dimension, int64)], status) + end if + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%spacing_path, spacing, & + [int(record%dimension, int64)], status) + end if + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_uniform_grid_r4 + + subroutine writer_define_uniform_grid_r8(this, name, dimensions, origin, & + spacing, grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer(int64), intent(in) :: dimensions(:) + real(real64), intent(in) :: origin(:), spacing(:) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + + grid_id = make_grid_id(0) + call prepare_uniform_grid(this, name, dimensions, origin_size=size(origin), & + spacing_size=size(spacing), record=record, status=status, & + collection_id=collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL64 + + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%origin_path, origin, & + [int(record%dimension, int64)], status) + end if + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%spacing_path, spacing, & + [int(record%dimension, int64)], status) + end if + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_uniform_grid_r8 + + subroutine prepare_uniform_grid(this, name, dimensions, origin_size, & + spacing_size, record, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer(int64), intent(in) :: dimensions(:) + integer, intent(in) :: origin_size, spacing_size + type(grid_record_t), intent(out) :: record + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + call prepare_structured_grid(this, name, dimensions, record, status, & + collection_id) + if (status%is_error()) return + if (origin_size /= record%dimension .or. & + spacing_size /= record%dimension) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Uniform-grid origin and spacing must match the grid dimension') + return + end if + + record%geometry_type = XDMF_GEOMETRY_UNIFORM + if (record%dimension == 2) then + record%topology_type = XDMF_TOPOLOGY_2D_CORECTMESH + else + record%topology_type = XDMF_TOPOLOGY_3D_CORECTMESH + end if + record%origin_path = record%group_path//'/origin' + record%spacing_path = record%group_path//'/spacing' + end subroutine prepare_uniform_grid + + subroutine writer_define_rectilinear_grid_2d_r4(this, name, x, y, & + grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + real(real32), intent(in) :: x(:), y(:) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + + grid_id = make_grid_id(0) + call prepare_rectilinear_grid(this, name, & + [int(size(x), int64), int(size(y), int64)], record, status, & + collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL32 + call write_rectilinear_group_r4(this, record, x, y, status=status) + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_rectilinear_grid_2d_r4 + + subroutine writer_define_rectilinear_grid_2d_r8(this, name, x, y, & + grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + real(real64), intent(in) :: x(:), y(:) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + + grid_id = make_grid_id(0) + call prepare_rectilinear_grid(this, name, & + [int(size(x), int64), int(size(y), int64)], record, status, & + collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL64 + call write_rectilinear_group_r8(this, record, x, y, status=status) + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_rectilinear_grid_2d_r8 + + subroutine writer_define_rectilinear_grid_3d_r4(this, name, x, y, z, & + grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + real(real32), intent(in) :: x(:), y(:), z(:) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + + grid_id = make_grid_id(0) + call prepare_rectilinear_grid(this, name, & + [int(size(x), int64), int(size(y), int64), int(size(z), int64)], & + record, status, collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL32 + call write_rectilinear_group_r4(this, record, x, y, z, status) + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_rectilinear_grid_3d_r4 + + subroutine writer_define_rectilinear_grid_3d_r8(this, name, x, y, z, & + grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + real(real64), intent(in) :: x(:), y(:), z(:) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + + grid_id = make_grid_id(0) + call prepare_rectilinear_grid(this, name, & + [int(size(x), int64), int(size(y), int64), int(size(z), int64)], & + record, status, collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL64 + call write_rectilinear_group_r8(this, record, x, y, z, status) + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_rectilinear_grid_3d_r8 + + subroutine prepare_rectilinear_grid(this, name, dimensions, record, & + status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer(int64), intent(in) :: dimensions(:) + type(grid_record_t), intent(out) :: record + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + integer :: path_length + + call prepare_structured_grid(this, name, dimensions, record, status, & + collection_id) + if (status%is_error()) return + record%geometry_type = XDMF_GEOMETRY_RECTILINEAR + if (record%dimension == 2) then + record%topology_type = XDMF_TOPOLOGY_2D_RECTMESH + else + record%topology_type = XDMF_TOPOLOGY_3D_RECTMESH + end if + record%axis_sizes = dimensions + path_length = len(record%group_path) + len('/axis_x') + allocate(character(len=path_length) :: & + record%axis_paths(record%dimension)) + record%axis_paths(1) = record%group_path//'/axis_x' + record%axis_paths(2) = record%group_path//'/axis_y' + if (record%dimension == 3) then + record%axis_paths(3) = record%group_path//'/axis_z' + end if + end subroutine prepare_rectilinear_grid + + subroutine write_rectilinear_group_r4(this, record, x, y, z, status) + class(xdmf_writer_t), intent(inout) :: this + type(grid_record_t), intent(in) :: record + real(real32), intent(in) :: x(:), y(:) + real(real32), intent(in), optional :: z(:) + type(xdmf_status_t), intent(out) :: status + + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%axis_paths(1), x, & + [int(size(x), int64)], status) + end if + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%axis_paths(2), y, & + [int(size(y), int64)], status) + end if + if (present(z) .and. .not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%axis_paths(3), z, & + [int(size(z), int64)], status) + end if + end subroutine write_rectilinear_group_r4 + + subroutine write_rectilinear_group_r8(this, record, x, y, z, status) + class(xdmf_writer_t), intent(inout) :: this + type(grid_record_t), intent(in) :: record + real(real64), intent(in) :: x(:), y(:) + real(real64), intent(in), optional :: z(:) + type(xdmf_status_t), intent(out) :: status + + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%axis_paths(1), x, & + [int(size(x), int64)], status) + end if + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%axis_paths(2), y, & + [int(size(y), int64)], status) + end if + if (present(z) .and. .not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%axis_paths(3), z, & + [int(size(z), int64)], status) + end if + end subroutine write_rectilinear_group_r8 + + subroutine writer_define_curvilinear_grid_r4(this, name, dimensions, & + points, grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer(int64), intent(in) :: dimensions(:) + real(real32), intent(in) :: points(:, :) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + integer(int64), allocatable :: shape(:) + + grid_id = make_grid_id(0) + call prepare_curvilinear_grid(this, name, dimensions, size(points, 1), & + size(points, 2), record, status, collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL32 + shape = [int(record%dimension, int64), dimensions] + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%points_path, & + reshape(points, [size(points)]), shape, status) + end if + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_curvilinear_grid_r4 + + subroutine writer_define_curvilinear_grid_r8(this, name, dimensions, & + points, grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer(int64), intent(in) :: dimensions(:) + real(real64), intent(in) :: points(:, :) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + integer(int64), allocatable :: shape(:) + + grid_id = make_grid_id(0) + call prepare_curvilinear_grid(this, name, dimensions, size(points, 1), & + size(points, 2), record, status, collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL64 + shape = [int(record%dimension, int64), dimensions] + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%points_path, & + reshape(points, [size(points)]), shape, status) + end if + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_curvilinear_grid_r8 + + subroutine prepare_curvilinear_grid(this, name, dimensions, point_dimension, & + point_count, record, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer(int64), intent(in) :: dimensions(:) + integer, intent(in) :: point_dimension, point_count + type(grid_record_t), intent(out) :: record + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + call prepare_structured_grid(this, name, dimensions, record, status, & + collection_id) + if (status%is_error()) return + if (point_dimension /= record%dimension .or. & + int(point_count, int64) /= record%number_of_points) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Curvilinear points must have shape (dimension, number_of_points)') + return + end if + + record%geometry_type = XDMF_GEOMETRY_CURVILINEAR + if (record%dimension == 2) then + record%topology_type = XDMF_TOPOLOGY_2D_SMESH + else + record%topology_type = XDMF_TOPOLOGY_3D_SMESH + end if + record%points_path = record%group_path//'/points' + end subroutine prepare_curvilinear_grid + + subroutine writer_define_unstructured_grid_r4(this, name, topology, & + points, connectivity, grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer, intent(in) :: topology + real(real32), intent(in) :: points(:, :) + integer(int64), intent(in) :: connectivity(:, :) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + integer(int64), allocatable :: zero_connectivity(:, :) + + grid_id = make_grid_id(0) + call prepare_unstructured_grid(this, name, topology, size(points, 1), & + size(points, 2), connectivity, record, zero_connectivity, status, & + collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL32 + call write_unstructured_group_r4(this, record, points, & + zero_connectivity, status) + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_unstructured_grid_r4 + + subroutine writer_define_unstructured_grid_r8(this, name, topology, & + points, connectivity, grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer, intent(in) :: topology + real(real64), intent(in) :: points(:, :) + integer(int64), intent(in) :: connectivity(:, :) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + integer(int64), allocatable :: zero_connectivity(:, :) + + grid_id = make_grid_id(0) + call prepare_unstructured_grid(this, name, topology, size(points, 1), & + size(points, 2), connectivity, record, zero_connectivity, status, & + collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL64 + call write_unstructured_group_r8(this, record, points, & + zero_connectivity, status) + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_unstructured_grid_r8 + + subroutine prepare_unstructured_grid(this, name, topology, point_dimension, & + point_count, connectivity, record, zero_connectivity, status, & + collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer, intent(in) :: topology, point_dimension, point_count + integer(int64), intent(in) :: connectivity(:, :) + type(grid_record_t), intent(out) :: record + integer(int64), allocatable, intent(out) :: zero_connectivity(:, :) + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + integer :: expected_nodes + + call prepare_grid_record(this, name, record, status, collection_id) + if (status%is_error()) return + if (point_dimension /= 2 .and. point_dimension /= 3) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Unstructured points must have dimension 2 or 3') + return + end if + if (point_count <= 0 .or. size(connectivity, 1) <= 0 .or. & + size(connectivity, 2) <= 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Unstructured points and connectivity must not be empty') + return + end if + if (.not. topology_is_supported(topology) .or. & + topology == XDMF_TOPOLOGY_MIXED .or. & + topology == XDMF_TOPOLOGY_POLYHEDRON .or. & + is_structured_topology(topology)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Unsupported fixed unstructured topology: '//topology_name(topology)) + return + end if + + expected_nodes = topology_nodes_per_element(topology) + if (expected_nodes > 0 .and. size(connectivity, 1) /= expected_nodes) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Connectivity node count does not match the topology') + return + end if + if (topology == XDMF_TOPOLOGY_POLYLINE .and. & + size(connectivity, 1) < 2) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'A polyline requires at least two nodes') + return + end if + if (topology == XDMF_TOPOLOGY_POLYGON .and. & + size(connectivity, 1) < 3) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'A polygon requires at least three nodes') + return + end if + call validate_node_indices(connectivity, int(point_count, int64), status) + if (status%is_error()) return + + zero_connectivity = connectivity - 1_int64 + record%geometry_type = XDMF_GEOMETRY_UNSTRUCTURED + record%topology_type = topology + record%dimension = point_dimension + record%nodes_per_element = size(connectivity, 1) + record%number_of_points = int(point_count, int64) + record%number_of_elements = int(size(connectivity, 2), int64) + record%points_path = record%group_path//'/points' + record%connectivity_path = record%group_path//'/connectivity' + end subroutine prepare_unstructured_grid + + subroutine write_unstructured_group_r4(this, record, points, connectivity, & + status) + class(xdmf_writer_t), intent(inout) :: this + type(grid_record_t), intent(in) :: record + real(real32), intent(in) :: points(:, :) + integer(int64), intent(in) :: connectivity(:, :) + type(xdmf_status_t), intent(out) :: status + + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%points_path, & + reshape(points, [size(points)]), & + [int(size(points, 1), int64), int(size(points, 2), int64)], status) + end if + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%connectivity_path, & + reshape(connectivity, [size(connectivity)]), & + [int(size(connectivity, 1), int64), & + int(size(connectivity, 2), int64)], status) + end if + end subroutine write_unstructured_group_r4 + + subroutine write_unstructured_group_r8(this, record, points, connectivity, & + status) + class(xdmf_writer_t), intent(inout) :: this + type(grid_record_t), intent(in) :: record + real(real64), intent(in) :: points(:, :) + integer(int64), intent(in) :: connectivity(:, :) + type(xdmf_status_t), intent(out) :: status + + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%points_path, & + reshape(points, [size(points)]), & + [int(size(points, 1), int64), int(size(points, 2), int64)], status) + end if + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%connectivity_path, & + reshape(connectivity, [size(connectivity)]), & + [int(size(connectivity, 1), int64), & + int(size(connectivity, 2), int64)], status) + end if + end subroutine write_unstructured_group_r8 + + subroutine writer_define_mixed_grid_r4(this, name, points, connectivity, & + grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + real(real32), intent(in) :: points(:, :) + integer(int64), intent(in) :: connectivity(:) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + integer(int64), allocatable :: zero_connectivity(:) + + grid_id = make_grid_id(0) + call prepare_mixed_grid(this, name, size(points, 1), size(points, 2), & + connectivity, record, zero_connectivity, status, collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL32 + call write_mixed_group_r4(this, record, points, zero_connectivity, status) + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_mixed_grid_r4 + + subroutine writer_define_mixed_grid_r8(this, name, points, connectivity, & + grid_id, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + real(real64), intent(in) :: points(:, :) + integer(int64), intent(in) :: connectivity(:) + type(xdmf_grid_id_t), intent(out) :: grid_id + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + type(grid_record_t) :: record + integer(int64), allocatable :: zero_connectivity(:) + + grid_id = make_grid_id(0) + call prepare_mixed_grid(this, name, size(points, 1), size(points, 2), & + connectivity, record, zero_connectivity, status, collection_id) + if (status%is_error()) return + record%geometry_numeric_type = XDMF_NUMERIC_REAL64 + call write_mixed_group_r8(this, record, points, zero_connectivity, status) + call finish_grid_definition(this, record, grid_id, status) + end subroutine writer_define_mixed_grid_r8 + + subroutine prepare_mixed_grid(this, name, point_dimension, point_count, & + connectivity, record, zero_connectivity, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer, intent(in) :: point_dimension, point_count + integer(int64), intent(in) :: connectivity(:) + type(grid_record_t), intent(out) :: record + integer(int64), allocatable, intent(out) :: zero_connectivity(:) + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + integer(int64) :: element_count + + call prepare_grid_record(this, name, record, status, collection_id) + if (status%is_error()) return + if ((point_dimension /= 2 .and. point_dimension /= 3) .or. & + point_count <= 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Mixed-grid points must be a nonempty dimension-by-points array') + return + end if + + call convert_mixed_connectivity(connectivity, int(point_count, int64), & + zero_connectivity, element_count, status) + if (status%is_error()) return + + record%geometry_type = XDMF_GEOMETRY_UNSTRUCTURED + record%topology_type = XDMF_TOPOLOGY_MIXED + record%dimension = point_dimension + record%number_of_points = int(point_count, int64) + record%number_of_elements = element_count + record%mixed_connectivity_size = int(size(connectivity), int64) + record%points_path = record%group_path//'/points' + record%connectivity_path = record%group_path//'/connectivity' + end subroutine prepare_mixed_grid + + subroutine write_mixed_group_r4(this, record, points, connectivity, status) + class(xdmf_writer_t), intent(inout) :: this + type(grid_record_t), intent(in) :: record + real(real32), intent(in) :: points(:, :) + integer(int64), intent(in) :: connectivity(:) + type(xdmf_status_t), intent(out) :: status + + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%points_path, & + reshape(points, [size(points)]), & + [int(size(points, 1), int64), int(size(points, 2), int64)], status) + end if + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%connectivity_path, & + connectivity, [int(size(connectivity), int64)], status) + end if + end subroutine write_mixed_group_r4 + + subroutine write_mixed_group_r8(this, record, points, connectivity, status) + class(xdmf_writer_t), intent(inout) :: this + type(grid_record_t), intent(in) :: record + real(real64), intent(in) :: points(:, :) + integer(int64), intent(in) :: connectivity(:) + type(xdmf_status_t), intent(out) :: status + + call hdf_create_group(this%hdf5_file, record%group_path, status) + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%points_path, & + reshape(points, [size(points)]), & + [int(size(points, 1), int64), int(size(points, 2), int64)], status) + end if + if (.not. status%is_error()) then + call hdf_write_dataset(this%hdf5_file, record%connectivity_path, & + connectivity, [int(size(connectivity), int64)], status) + end if + end subroutine write_mixed_group_r8 + + subroutine writer_define_attribute_with_series(this, grid_id, name, & + center, attribute_type, numeric_type, is_series, attribute_id, status, & + entity_count, component_shape) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_grid_id_t), intent(in) :: grid_id + character(len=*), intent(in) :: name + integer, intent(in) :: center, attribute_type, numeric_type + logical, intent(in) :: is_series + type(xdmf_attribute_id_t), intent(out) :: attribute_id + type(xdmf_status_t), intent(out) :: status + integer(int64), intent(in), optional :: entity_count + integer(int64), intent(in), optional :: component_shape(:) + + call define_attribute_impl(this, grid_id, name, center, attribute_type, & + numeric_type, is_series, attribute_id, status, entity_count, & + component_shape) + end subroutine writer_define_attribute_with_series + + subroutine writer_define_static_attribute(this, grid_id, name, center, & + attribute_type, numeric_type, attribute_id, status, entity_count, & + component_shape) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_grid_id_t), intent(in) :: grid_id + character(len=*), intent(in) :: name + integer, intent(in) :: center, attribute_type, numeric_type + type(xdmf_attribute_id_t), intent(out) :: attribute_id + type(xdmf_status_t), intent(out) :: status + integer(int64), intent(in), optional :: entity_count + integer(int64), intent(in), optional :: component_shape(:) + + call define_attribute_impl(this, grid_id, name, center, attribute_type, & + numeric_type, .false., attribute_id, status, entity_count, & + component_shape) + end subroutine writer_define_static_attribute + + subroutine define_attribute_impl(this, grid_id, name, center, & + attribute_type, numeric_type, is_series, attribute_id, status, & + entity_count, component_shape) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_grid_id_t), intent(in) :: grid_id + character(len=*), intent(in) :: name + integer, intent(in) :: center, attribute_type, numeric_type + logical, intent(in) :: is_series + type(xdmf_attribute_id_t), intent(out) :: attribute_id + type(xdmf_status_t), intent(out) :: status + integer(int64), intent(in), optional :: entity_count + integer(int64), intent(in), optional :: component_shape(:) + + type(attribute_record_t) :: record + character(len=:), allocatable :: group_path + integer :: grid_index, index + + attribute_id = make_attribute_id(0) + call check_definition_state(this, name, status) + if (status%is_error()) return + grid_index = find_grid(this, grid_id) + if (grid_index == 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'The attribute references an unknown grid') + return + end if + if (len(center_name(center)) == 0 .or. & + len(attribute_type_name(attribute_type)) == 0 .or. & + len(numeric_type_name(numeric_type)) == 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Invalid attribute center, type, or numeric type') + return + end if + if (is_series .and. this%options%series_kind == XDMF_SERIES_NONE) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Series attributes require a series-enabled writer') + return + end if + if (attribute_type == XDMF_ATTRIBUTE_GLOBAL_ID .and. & + numeric_type /= XDMF_NUMERIC_INT32 .and. & + numeric_type /= XDMF_NUMERIC_INT64) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'GlobalID attributes require an integer numeric type') + return + end if + do index = 1, size(this%attributes) + if (this%attributes(index)%grid_id == grid_id_value(grid_id) .and. & + this%attributes(index)%name == trim(name)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Attribute names must be unique within a grid') + return + end if + end do + + record%id = this%next_attribute_id + record%grid_id = grid_id_value(grid_id) + record%name = trim(name) + record%center = center + record%attribute_type = attribute_type + record%numeric_type = numeric_type + record%is_series = is_series + record%last_step = this%committed_steps + call infer_attribute_shape(this%grids(grid_index), center, attribute_type, & + entity_count, component_shape, record, status) + if (status%is_error()) return + + group_path = indexed_path('/attributes/a', record%id) + record%dataset_path = group_path//'/values' + this%next_attribute_id = this%next_attribute_id + 1 + call hdf_create_group(this%hdf5_file, group_path, status) + if (status%is_error()) return + if (is_series) then + call hdf_create_series_dataset(this%hdf5_file, record%dataset_path, & + numeric_type, record%storage_shape, this%options, status) + if (status%is_error()) return + end if + + call append_attribute(this, record, status) + if (.not. status%is_error()) then + attribute_id = make_attribute_id(record%id, this%owner_token) + end if + end subroutine define_attribute_impl + + subroutine writer_write_attribute_r4(this, attribute_id, values, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + real(real32), intent(in) :: values(:) + type(xdmf_status_t), intent(out) :: status + + integer :: index + + call prepare_attribute_write(this, attribute_id, XDMF_NUMERIC_REAL32, & + size(values, kind=int64), index, status) + if (status%is_error()) return + call write_attribute_data_r4(this, index, values, status) + call finish_attribute_write(this, index, status) + end subroutine writer_write_attribute_r4 + + subroutine writer_write_attribute_r8(this, attribute_id, values, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + real(real64), intent(in) :: values(:) + type(xdmf_status_t), intent(out) :: status + + integer :: index + + call prepare_attribute_write(this, attribute_id, XDMF_NUMERIC_REAL64, & + size(values, kind=int64), index, status) + if (status%is_error()) return + call write_attribute_data_r8(this, index, values, status) + call finish_attribute_write(this, index, status) + end subroutine writer_write_attribute_r8 + + subroutine writer_write_attribute_i4(this, attribute_id, values, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + integer(int32), intent(in) :: values(:) + type(xdmf_status_t), intent(out) :: status + + integer :: index + + call prepare_attribute_write(this, attribute_id, XDMF_NUMERIC_INT32, & + size(values, kind=int64), index, status) + if (status%is_error()) return + call write_attribute_data_i4(this, index, values, status) + call finish_attribute_write(this, index, status) + end subroutine writer_write_attribute_i4 + + subroutine writer_write_attribute_i8(this, attribute_id, values, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + integer(int64), intent(in) :: values(:) + type(xdmf_status_t), intent(out) :: status + + integer :: index + + call prepare_attribute_write(this, attribute_id, XDMF_NUMERIC_INT64, & + size(values, kind=int64), index, status) + if (status%is_error()) return + call write_attribute_data_i8(this, index, values, status) + call finish_attribute_write(this, index, status) + end subroutine writer_write_attribute_i8 + + subroutine writer_write_attribute_hyperslab_r4(this, attribute_id, values, & + spatial_offset, spatial_count, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + real(real32), intent(in) :: values(:) + integer(int64), intent(in) :: spatial_offset(:), spatial_count(:) + type(xdmf_status_t), intent(out) :: status + + integer(int64), allocatable :: storage_offset(:), storage_count(:) + integer :: index + + call prepare_attribute_hyperslab_write(this, attribute_id, & + XDMF_NUMERIC_REAL32, size(values, kind=int64), spatial_offset, & + spatial_count, index, storage_offset, storage_count, status) + if (status%is_error()) return + if (this%attributes(index)%is_series) then + call hdf_append_series_hyperslab(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, storage_offset, storage_count, & + this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, status, storage_offset, & + storage_count) + end if + call synchronize_collective_status(this, status, & + 'Collective attribute hyperslab write failed') + call finish_attribute_write(this, index, status) + end subroutine writer_write_attribute_hyperslab_r4 + + subroutine writer_write_attribute_hyperslab_r8(this, attribute_id, values, & + spatial_offset, spatial_count, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + real(real64), intent(in) :: values(:) + integer(int64), intent(in) :: spatial_offset(:), spatial_count(:) + type(xdmf_status_t), intent(out) :: status + + integer(int64), allocatable :: storage_offset(:), storage_count(:) + integer :: index + + call prepare_attribute_hyperslab_write(this, attribute_id, & + XDMF_NUMERIC_REAL64, size(values, kind=int64), spatial_offset, & + spatial_count, index, storage_offset, storage_count, status) + if (status%is_error()) return + if (this%attributes(index)%is_series) then + call hdf_append_series_hyperslab(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, storage_offset, storage_count, & + this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, status, storage_offset, & + storage_count) + end if + call synchronize_collective_status(this, status, & + 'Collective attribute hyperslab write failed') + call finish_attribute_write(this, index, status) + end subroutine writer_write_attribute_hyperslab_r8 + + subroutine writer_write_attribute_hyperslab_i4(this, attribute_id, values, & + spatial_offset, spatial_count, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + integer(int32), intent(in) :: values(:) + integer(int64), intent(in) :: spatial_offset(:), spatial_count(:) + type(xdmf_status_t), intent(out) :: status + + integer(int64), allocatable :: storage_offset(:), storage_count(:) + integer :: index + + call prepare_attribute_hyperslab_write(this, attribute_id, & + XDMF_NUMERIC_INT32, size(values, kind=int64), spatial_offset, & + spatial_count, index, storage_offset, storage_count, status) + if (status%is_error()) return + if (this%attributes(index)%is_series) then + call hdf_append_series_hyperslab(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, storage_offset, storage_count, & + this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, status, storage_offset, & + storage_count) + end if + call synchronize_collective_status(this, status, & + 'Collective attribute hyperslab write failed') + call finish_attribute_write(this, index, status) + end subroutine writer_write_attribute_hyperslab_i4 + + subroutine writer_write_attribute_hyperslab_i8(this, attribute_id, values, & + spatial_offset, spatial_count, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + integer(int64), intent(in) :: values(:) + integer(int64), intent(in) :: spatial_offset(:), spatial_count(:) + type(xdmf_status_t), intent(out) :: status + + integer(int64), allocatable :: storage_offset(:), storage_count(:) + integer :: index + + call prepare_attribute_hyperslab_write(this, attribute_id, & + XDMF_NUMERIC_INT64, size(values, kind=int64), spatial_offset, & + spatial_count, index, storage_offset, storage_count, status) + if (status%is_error()) return + if (this%attributes(index)%is_series) then + call hdf_append_series_hyperslab(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, storage_offset, storage_count, & + this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, status, storage_offset, & + storage_count) + end if + call synchronize_collective_status(this, status, & + 'Collective attribute hyperslab write failed') + call finish_attribute_write(this, index, status) + end subroutine writer_write_attribute_hyperslab_i8 + + subroutine write_attribute_data_r4(this, index, values, status) + class(xdmf_writer_t), intent(inout) :: this + integer, intent(in) :: index + real(real32), intent(in) :: values(:) + type(xdmf_status_t), intent(out) :: status + + real(real32), allocatable :: packed(:) + + if (size(this%attributes(index)%component_shape) == 2) then + call pack_components_r4(values, this%attributes(index), packed, status) + if (status%is_error()) return + if (this%attributes(index)%is_series) then + call hdf_append_series(this%hdf5_file, & + this%attributes(index)%dataset_path, packed, & + this%attributes(index)%storage_shape, this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, packed, & + this%attributes(index)%storage_shape, status) + end if + else if (this%attributes(index)%is_series) then + call hdf_append_series(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, status) + end if + end subroutine write_attribute_data_r4 + + subroutine write_attribute_data_r8(this, index, values, status) + class(xdmf_writer_t), intent(inout) :: this + integer, intent(in) :: index + real(real64), intent(in) :: values(:) + type(xdmf_status_t), intent(out) :: status + + real(real64), allocatable :: packed(:) + + if (size(this%attributes(index)%component_shape) == 2) then + call pack_components_r8(values, this%attributes(index), packed, status) + if (status%is_error()) return + if (this%attributes(index)%is_series) then + call hdf_append_series(this%hdf5_file, & + this%attributes(index)%dataset_path, packed, & + this%attributes(index)%storage_shape, this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, packed, & + this%attributes(index)%storage_shape, status) + end if + else if (this%attributes(index)%is_series) then + call hdf_append_series(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, status) + end if + end subroutine write_attribute_data_r8 + + subroutine write_attribute_data_i4(this, index, values, status) + class(xdmf_writer_t), intent(inout) :: this + integer, intent(in) :: index + integer(int32), intent(in) :: values(:) + type(xdmf_status_t), intent(out) :: status + + integer(int32), allocatable :: packed(:) + + if (size(this%attributes(index)%component_shape) == 2) then + call pack_components_i4(values, this%attributes(index), packed, status) + if (status%is_error()) return + if (this%attributes(index)%is_series) then + call hdf_append_series(this%hdf5_file, & + this%attributes(index)%dataset_path, packed, & + this%attributes(index)%storage_shape, this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, packed, & + this%attributes(index)%storage_shape, status) + end if + else if (this%attributes(index)%is_series) then + call hdf_append_series(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, status) + end if + end subroutine write_attribute_data_i4 + + subroutine write_attribute_data_i8(this, index, values, status) + class(xdmf_writer_t), intent(inout) :: this + integer, intent(in) :: index + integer(int64), intent(in) :: values(:) + type(xdmf_status_t), intent(out) :: status + + integer(int64), allocatable :: packed(:) + + if (size(this%attributes(index)%component_shape) == 2) then + call pack_components_i8(values, this%attributes(index), packed, status) + if (status%is_error()) return + if (this%attributes(index)%is_series) then + call hdf_append_series(this%hdf5_file, & + this%attributes(index)%dataset_path, packed, & + this%attributes(index)%storage_shape, this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, packed, & + this%attributes(index)%storage_shape, status) + end if + else if (this%attributes(index)%is_series) then + call hdf_append_series(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, this%committed_steps, status) + else + call hdf_write_dataset(this%hdf5_file, & + this%attributes(index)%dataset_path, values, & + this%attributes(index)%storage_shape, status) + end if + end subroutine write_attribute_data_i8 + + subroutine pack_components_r4(values, attribute, packed, status) + real(real32), intent(in) :: values(:) + type(attribute_record_t), intent(in) :: attribute + real(real32), allocatable, intent(out) :: packed(:) + type(xdmf_status_t), intent(out) :: status + + integer :: allocation_status, column, entity, input_index, output_index + integer :: row, rows, columns, values_per_entity + + allocate(packed(size(values)), stat=allocation_status) + if (allocation_status /= 0) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Could not allocate attribute packing storage') + return + end if + if (size(attribute%component_shape) /= 2) then + packed = values + call set_status_success(status) + return + end if + + rows = int(attribute%component_shape(1)) + columns = int(attribute%component_shape(2)) + values_per_entity = rows * columns + do entity = 0, int(attribute%entity_count) - 1 + do column = 1, columns + do row = 1, rows + input_index = entity * values_per_entity + row + (column - 1) * rows + output_index = entity * values_per_entity + column + (row - 1) * columns + packed(output_index) = values(input_index) + end do + end do + end do + call set_status_success(status) + end subroutine pack_components_r4 + + subroutine pack_components_r8(values, attribute, packed, status) + real(real64), intent(in) :: values(:) + type(attribute_record_t), intent(in) :: attribute + real(real64), allocatable, intent(out) :: packed(:) + type(xdmf_status_t), intent(out) :: status + + integer :: allocation_status, column, entity, input_index, output_index + integer :: row, rows, columns, values_per_entity + + allocate(packed(size(values)), stat=allocation_status) + if (allocation_status /= 0) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Could not allocate attribute packing storage') + return + end if + if (size(attribute%component_shape) /= 2) then + packed = values + call set_status_success(status) + return + end if + + rows = int(attribute%component_shape(1)) + columns = int(attribute%component_shape(2)) + values_per_entity = rows * columns + do entity = 0, int(attribute%entity_count) - 1 + do column = 1, columns + do row = 1, rows + input_index = entity * values_per_entity + row + (column - 1) * rows + output_index = entity * values_per_entity + column + (row - 1) * columns + packed(output_index) = values(input_index) + end do + end do + end do + call set_status_success(status) + end subroutine pack_components_r8 + + subroutine pack_components_i4(values, attribute, packed, status) + integer(int32), intent(in) :: values(:) + type(attribute_record_t), intent(in) :: attribute + integer(int32), allocatable, intent(out) :: packed(:) + type(xdmf_status_t), intent(out) :: status + + integer :: allocation_status, column, entity, input_index, output_index + integer :: row, rows, columns, values_per_entity + + allocate(packed(size(values)), stat=allocation_status) + if (allocation_status /= 0) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Could not allocate attribute packing storage') + return + end if + if (size(attribute%component_shape) /= 2) then + packed = values + call set_status_success(status) + return + end if + + rows = int(attribute%component_shape(1)) + columns = int(attribute%component_shape(2)) + values_per_entity = rows * columns + do entity = 0, int(attribute%entity_count) - 1 + do column = 1, columns + do row = 1, rows + input_index = entity * values_per_entity + row + (column - 1) * rows + output_index = entity * values_per_entity + column + (row - 1) * columns + packed(output_index) = values(input_index) + end do + end do + end do + call set_status_success(status) + end subroutine pack_components_i4 + + subroutine pack_components_i8(values, attribute, packed, status) + integer(int64), intent(in) :: values(:) + type(attribute_record_t), intent(in) :: attribute + integer(int64), allocatable, intent(out) :: packed(:) + type(xdmf_status_t), intent(out) :: status + + integer :: allocation_status, column, entity, input_index, output_index + integer :: row, rows, columns, values_per_entity + + allocate(packed(size(values)), stat=allocation_status) + if (allocation_status /= 0) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Could not allocate attribute packing storage') + return + end if + if (size(attribute%component_shape) /= 2) then + packed = values + call set_status_success(status) + return + end if + + rows = int(attribute%component_shape(1)) + columns = int(attribute%component_shape(2)) + values_per_entity = rows * columns + do entity = 0, int(attribute%entity_count) - 1 + do column = 1, columns + do row = 1, rows + input_index = entity * values_per_entity + row + (column - 1) * rows + output_index = entity * values_per_entity + column + (row - 1) * columns + packed(output_index) = values(input_index) + end do + end do + end do + call set_status_success(status) + end subroutine pack_components_i8 + + subroutine writer_begin_step(this, value, status) + class(xdmf_writer_t), intent(inout) :: this + real(real64), intent(in) :: value + type(xdmf_status_t), intent(out) :: status + + call check_open(this, status) + if (status%is_error()) return + if (this%options%series_kind == XDMF_SERIES_NONE) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'This writer was not created for series output') + return + end if + if (this%step_is_active) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'A series step is already active') + return + end if + + this%definitions_locked = .true. + this%step_is_active = .true. + this%active_step_value = value + call set_status_success(status) + end subroutine writer_begin_step + + subroutine writer_end_step(this, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_status_t), intent(out) :: status + + type(xdmf_status_t) :: original_status, rollback_status + real(real64), allocatable :: new_values(:) + real(real64) :: step_value(1) + integer(int64) :: scalar_shape(0) + integer :: allocation_status, index, next_step + + call check_open(this, status) + if (status%is_error()) return + if (.not. this%step_is_active) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'No series step is active') + return + end if + + next_step = this%committed_steps + 1 + do index = 1, size(this%attributes) + if (this%attributes(index)%is_series .and. & + this%attributes(index)%last_step /= next_step) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Every series attribute must be written exactly once per step') + call rollback_active_step(this, rollback_status) + if (rollback_status%is_error()) status = rollback_status + return + end if + end do + + allocate(new_values(this%committed_steps + 1), stat=allocation_status) + if (allocation_status /= 0) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Could not extend the in-memory series metadata') + call rollback_active_step(this, rollback_status) + if (rollback_status%is_error()) status = rollback_status + return + end if + if (this%committed_steps > 0) then + new_values(:this%committed_steps) = this%series_values + end if + new_values(this%committed_steps + 1) = this%active_step_value + + step_value(1) = this%active_step_value + call hdf_append_series(this%hdf5_file, SERIES_VALUES_PATH, step_value, & + scalar_shape, this%committed_steps, status) + call synchronize_collective_status(this, status, & + 'Collective series-coordinate write failed') + if (status%is_error()) then + original_status = status + if (status%error_code() == XDMF_ERROR_CONSISTENCY) then + this%is_poisoned = .true. + end if + call rollback_active_step(this, rollback_status) + if (rollback_status%is_error()) then + status = rollback_status + else + status = original_status + end if + return + end if + + call move_alloc(new_values, this%series_values) + this%committed_steps = next_step + this%step_is_active = .false. + call set_status_success(status) + end subroutine writer_end_step + + subroutine writer_flush(this, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_status_t), intent(out) :: status + + integer :: index + + call check_open(this, status) + if (status%is_error()) return + if (this%step_is_active) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Cannot flush while a series step is active') + return + end if + do index = 1, size(this%attributes) + if (.not. this%attributes(index)%is_series .and. & + .not. this%attributes(index)%is_written) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Every static attribute must be written before flushing') + return + end if + end do + + call hdf_flush_file(this%hdf5_file, status) + call synchronize_collective_status(this, status, & + 'Collective HDF5 flush failed') + if (status%is_error()) return + if (.not. this%is_collective .or. this%rank == this%root_rank) then + call write_xdmf_document(this%xdmf_path, this%hdf5_name, & + this%collections, this%grids, this%attributes, & + this%options%series_kind, this%series_values, status) + else + call set_status_success(status) + end if + call synchronize_collective_status(this, status, & + 'The collective-output root could not publish XDMF metadata') + end subroutine writer_flush + + subroutine writer_close(this, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_status_t), intent(out) :: status + + type(xdmf_status_t) :: operation_status, close_status + + call set_status_success(status) + if (.not. this%is_open) return + + if (this%is_poisoned) then + call hdf_close_file(this%hdf5_file, close_status) + call synchronize_collective_status(this, close_status, & + 'Collective HDF5 close failed') + if (close_status%is_error()) then + status = close_status + else + this%is_open = .false. + this%step_is_active = .false. + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Writer consistency was lost; HDF5 closed without writing XDMF') + end if + return + end if + + if (this%step_is_active) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Cannot close while a series step is active') + call rollback_active_step(this, operation_status) + if (operation_status%is_error()) status = operation_status + return + end if + + call writer_flush(this, operation_status) + if (operation_status%is_error()) then + status = operation_status + return + end if + + call hdf_close_file(this%hdf5_file, close_status) + call synchronize_collective_status(this, close_status, & + 'Collective HDF5 close failed') + if (close_status%is_error()) then + status = close_status + else + this%is_open = .false. + this%step_is_active = .false. + end if + end subroutine writer_close + + impure elemental subroutine writer_finalize(this) + type(xdmf_writer_t), intent(inout) :: this + + type(xdmf_status_t) :: status + + if (this%is_open) call hdf_close_file(this%hdf5_file, status) + if (.not. status%is_error()) this%is_open = .false. + end subroutine writer_finalize + + subroutine prepare_structured_grid(this, name, dimensions, record, status, & + collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + integer(int64), intent(in) :: dimensions(:) + type(grid_record_t), intent(out) :: record + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + call prepare_grid_record(this, name, record, status, collection_id) + if (status%is_error()) return + if ((size(dimensions) /= 2 .and. size(dimensions) /= 3) .or. & + any(dimensions <= 0_int64)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Structured dimensions must contain positive I/J or I/J/K sizes') + return + end if + + record%dimension = size(dimensions) + record%dimensions = dimensions + record%number_of_points = product_int64(dimensions) + if (record%number_of_points < 0_int64) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Structured grid dimensions overflow int64') + return + end if + if (all(dimensions > 1_int64)) then + record%number_of_elements = product_int64(dimensions - 1_int64) + if (record%number_of_elements < 0_int64) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Structured cell dimensions overflow int64') + return + end if + else + record%number_of_elements = 0_int64 + end if + end subroutine prepare_structured_grid + + subroutine prepare_grid_record(this, name, record, status, collection_id) + class(xdmf_writer_t), intent(inout) :: this + character(len=*), intent(in) :: name + type(grid_record_t), intent(out) :: record + type(xdmf_status_t), intent(out) :: status + type(xdmf_collection_id_t), intent(in), optional :: collection_id + + integer :: collection_value, index + + call check_definition_state(this, name, status) + if (status%is_error()) return + collection_value = 0 + if (present(collection_id)) then + if (collection_id_owner(collection_id) /= this%owner_token) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'The collection identifier belongs to another writer') + return + end if + collection_value = collection_id_value(collection_id) + if (collection_value /= 0 .and. & + .not. collection_exists(this, collection_value)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'The grid references an unknown collection') + return + end if + end if + do index = 1, size(this%grids) + if (this%grids(index)%name == trim(name)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Grid names must be unique') + return + end if + end do + + record%id = this%next_grid_id + record%collection_id = collection_value + record%name = trim(name) + record%group_path = indexed_path('/grids/g', record%id) + this%next_grid_id = this%next_grid_id + 1 + call set_status_success(status) + end subroutine prepare_grid_record + + subroutine finish_grid_definition(this, record, grid_id, status) + class(xdmf_writer_t), intent(inout) :: this + type(grid_record_t), intent(in) :: record + type(xdmf_grid_id_t), intent(inout) :: grid_id + type(xdmf_status_t), intent(inout) :: status + + if (status%is_error()) return + call append_grid(this, record, status) + if (.not. status%is_error()) then + grid_id = make_grid_id(record%id, this%owner_token) + end if + end subroutine finish_grid_definition + + subroutine infer_attribute_shape(grid, center, attribute_type, entity_count, & + requested_components, attribute, status) + type(grid_record_t), intent(in) :: grid + integer, intent(in) :: center, attribute_type + integer(int64), intent(in), optional :: entity_count + integer(int64), intent(in), optional :: requested_components(:) + type(attribute_record_t), intent(inout) :: attribute + type(xdmf_status_t), intent(out) :: status + + integer(int64), allocatable :: spatial_shape(:), components(:) + integer :: component_rank + + call set_status_success(status) + select case (center) + case (XDMF_CENTER_NODE) + if (allocated(grid%dimensions)) then + spatial_shape = grid%dimensions + else + spatial_shape = [grid%number_of_points] + end if + case (XDMF_CENTER_CELL) + if (allocated(grid%dimensions)) then + if (any(grid%dimensions <= 1_int64)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'A cell-centred attribute requires at least one structured cell') + return + end if + spatial_shape = grid%dimensions - 1_int64 + else + spatial_shape = [grid%number_of_elements] + end if + case (XDMF_CENTER_GRID) + spatial_shape = [1_int64] + case (XDMF_CENTER_EDGE, XDMF_CENTER_FACE) + if (.not. present(entity_count)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Edge- and face-centred attributes require entity_count') + return + end if + if (entity_count <= 0_int64) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Edge- and face-centred attributes require positive entity_count') + return + end if + spatial_shape = [entity_count] + end select + if (any(spatial_shape <= 0_int64)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'The attribute center has no entities on this grid') + return + end if + if (present(entity_count)) then + if (center /= XDMF_CENTER_EDGE .and. center /= XDMF_CENTER_FACE) then + if (entity_count /= product_int64(spatial_shape)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'entity_count conflicts with the inferred attribute shape') + return + end if + end if + end if + + if (present(requested_components)) then + if (any(requested_components <= 0_int64)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Attribute component dimensions must be positive') + return + end if + select case (attribute_type) + case (XDMF_ATTRIBUTE_SCALAR, XDMF_ATTRIBUTE_GLOBAL_ID) + if (size(requested_components) /= 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Scalar and GlobalID attributes cannot have component dimensions') + return + end if + case (XDMF_ATTRIBUTE_VECTOR) + if (size(requested_components) /= 1) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Vector attributes require one component dimension') + return + end if + case (XDMF_ATTRIBUTE_TENSOR) + if (size(requested_components) /= 2) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Tensor attributes require component shape [3, 3]') + return + end if + if (any(requested_components /= [3_int64, 3_int64])) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Tensor attributes require component shape [3, 3]') + return + end if + case (XDMF_ATTRIBUTE_TENSOR6) + if (size(requested_components) /= 1) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Tensor6 attributes require component shape [6]') + return + end if + if (requested_components(1) /= 6_int64) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Tensor6 attributes require component shape [6]') + return + end if + case (XDMF_ATTRIBUTE_MATRIX) + if (size(requested_components) /= 2) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Matrix attributes require two component dimensions') + return + end if + end select + components = requested_components + else + select case (attribute_type) + case (XDMF_ATTRIBUTE_SCALAR, XDMF_ATTRIBUTE_GLOBAL_ID) + allocate(components(0)) + case (XDMF_ATTRIBUTE_VECTOR) + allocate(components(1)) + components(1) = int(grid%dimension, int64) + case (XDMF_ATTRIBUTE_TENSOR) + allocate(components(2)) + components = 3_int64 + case (XDMF_ATTRIBUTE_TENSOR6) + allocate(components(1)) + components(1) = 6_int64 + case (XDMF_ATTRIBUTE_MATRIX) + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Matrix attributes require component_shape') + return + end select + end if + + component_rank = size(components) + allocate(attribute%component_shape(component_rank)) + if (component_rank > 0) attribute%component_shape = components + allocate(attribute%storage_shape(component_rank + size(spatial_shape))) + if (component_rank > 0) then + attribute%storage_shape(:component_rank) = & + components(component_rank:1:-1) + end if + attribute%storage_shape(component_rank + 1:) = spatial_shape + attribute%entity_count = product_int64(spatial_shape) + if (attribute%entity_count < 0_int64 .or. & + product_int64(attribute%storage_shape) < 0_int64) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Attribute dimensions overflow int64') + return + end if + end subroutine infer_attribute_shape + + subroutine prepare_attribute_write(this, attribute_id, numeric_type, & + value_count, index, status) + class(xdmf_writer_t), intent(in) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + integer, intent(in) :: numeric_type + integer(int64), intent(in) :: value_count + integer, intent(out) :: index + type(xdmf_status_t), intent(out) :: status + + call check_open(this, status) + if (status%is_error()) return + index = find_attribute(this, attribute_id) + if (index == 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Unknown attribute identifier') + return + end if + if (this%attributes(index)%numeric_type /= numeric_type) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Attribute values have the wrong numeric type') + return + end if + if (value_count /= product_int64(this%attributes(index)%storage_shape)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Attribute value count does not match its defined shape') + return + end if + + if (this%attributes(index)%is_series) then + if (.not. this%step_is_active) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'A series attribute can only be written inside an active step') + return + end if + if (this%attributes(index)%last_step == this%committed_steps + 1) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'A series attribute can only be written once per step') + return + end if + else if (this%attributes(index)%is_written) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'A static attribute can only be written once') + return + end if + end subroutine prepare_attribute_write + + subroutine prepare_attribute_hyperslab_write(this, attribute_id, & + numeric_type, value_count, spatial_offset, spatial_count, index, & + storage_offset, storage_count, status) + class(xdmf_writer_t), intent(in) :: this + type(xdmf_attribute_id_t), intent(in) :: attribute_id + integer, intent(in) :: numeric_type + integer(int64), intent(in) :: value_count + integer(int64), intent(in) :: spatial_offset(:), spatial_count(:) + integer, intent(out) :: index + integer(int64), allocatable, intent(out) :: storage_offset(:) + integer(int64), allocatable, intent(out) :: storage_count(:) + type(xdmf_status_t), intent(out) :: status + + integer(int64) :: expected_count + logical :: empty_selection + + index = 0 + call check_open(this, status) + if (.not. status%is_error() .and. .not. this%is_collective) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Attribute hyperslabs require collective HDF5 output') + end if + if (.not. status%is_error()) then + index = find_attribute(this, attribute_id) + if (index == 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Unknown attribute identifier') + end if + end if + if (.not. status%is_error()) then + if (this%attributes(index)%numeric_type /= numeric_type) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Attribute values have the wrong numeric type') + else if (size(this%attributes(index)%component_shape) /= 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Collective hyperslabs currently support scalar attributes only') + end if + end if + if (.not. status%is_error()) then + if (size(spatial_offset) /= size(this%attributes(index)%storage_shape) .or. & + size(spatial_count) /= size(this%attributes(index)%storage_shape)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Hyperslab offset and count must match the spatial rank') + else if (any(spatial_offset < 0_int64) .or. & + any(spatial_count < 0_int64)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Hyperslab offsets and counts must not be negative') + end if + end if + if (.not. status%is_error()) then + empty_selection = all(spatial_count == 0_int64) + if (.not. empty_selection .and. any(spatial_count == 0_int64)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'A hyperslab must be entirely empty or positive in every dimension') + else if (.not. empty_selection) then + if (any(spatial_count > this%attributes(index)%storage_shape) .or. & + any(spatial_offset > & + this%attributes(index)%storage_shape - spatial_count)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Attribute hyperslab lies outside the defined shape') + end if + end if + end if + if (.not. status%is_error()) then + if (all(spatial_count == 0_int64)) then + expected_count = 0_int64 + else + expected_count = product_int64(spatial_count) + end if + if (expected_count < 0_int64 .or. value_count /= expected_count) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Hyperslab value count does not match its local shape') + else if (this%attributes(index)%is_series) then + if (.not. this%step_is_active) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'A series attribute can only be written inside an active step') + else if (this%attributes(index)%last_step == & + this%committed_steps + 1) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'A series attribute can only be written once per step') + end if + else if (this%attributes(index)%is_written) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'A static attribute can only be written once') + end if + end if + + call synchronize_collective_status(this, status, & + 'Collective attribute hyperslab validation failed') + if (status%is_error()) return + storage_offset = spatial_offset + storage_count = spatial_count + end subroutine prepare_attribute_hyperslab_write + + subroutine finish_attribute_write(this, index, status) + class(xdmf_writer_t), intent(inout) :: this + integer, intent(in) :: index + type(xdmf_status_t), intent(inout) :: status + + type(xdmf_status_t) :: original_status, rollback_status + + if (status%is_error()) then + if (this%attributes(index)%is_series) then + original_status = status + call rollback_active_step(this, rollback_status) + if (rollback_status%is_error()) then + this%is_poisoned = .true. + status = rollback_status + else + status = original_status + if (status%error_code() == XDMF_ERROR_CONSISTENCY) then + this%is_poisoned = .true. + end if + end if + else if (status%error_code() == XDMF_ERROR_CONSISTENCY) then + this%is_poisoned = .true. + end if + return + end if + if (this%attributes(index)%is_series) then + this%attributes(index)%last_step = this%committed_steps + 1 + else + this%attributes(index)%is_written = .true. + end if + end subroutine finish_attribute_write + + subroutine rollback_active_step(this, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_status_t), intent(out) :: status + + type(xdmf_status_t) :: truncate_status + integer :: index + + call set_status_success(status) + do index = 1, size(this%attributes) + if (this%attributes(index)%is_series .and. & + this%attributes(index)%last_step > this%committed_steps) then + call hdf_truncate_series(this%hdf5_file, & + this%attributes(index)%dataset_path, & + this%attributes(index)%storage_shape, this%committed_steps, & + truncate_status) + if (truncate_status%is_error() .and. .not. status%is_error()) then + status = truncate_status + end if + if (truncate_status%is_error()) then + this%is_poisoned = .true. + else + this%attributes(index)%last_step = this%committed_steps + end if + end if + end do + this%step_is_active = .false. + end subroutine rollback_active_step + + subroutine convert_mixed_connectivity(input, number_of_points, output, & + number_of_elements, status) + integer(int64), intent(in) :: input(:) + integer(int64), intent(in) :: number_of_points + integer(int64), allocatable, intent(out) :: output(:) + integer(int64), intent(out) :: number_of_elements + type(xdmf_status_t), intent(out) :: status + + integer(int64) :: token, count_value + integer :: position, topology, node_count, face, face_count + + call set_status_success(status) + number_of_elements = 0_int64 + if (size(input) == 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Mixed connectivity must not be empty') + return + end if + output = input + position = 1 + do while (position <= size(input)) + token = input(position) + if (token < int(-huge(topology), int64) .or. & + token > int(huge(topology), int64)) then + call mixed_encoding_error(status) + return + end if + topology = int(token) + position = position + 1 + + select case (topology) + case (XDMF_TOPOLOGY_POLYVERTEX, XDMF_TOPOLOGY_POLYLINE, & + XDMF_TOPOLOGY_POLYGON) + if (position > size(input)) then + call mixed_encoding_error(status) + return + end if + count_value = input(position) + if (count_value > int(huge(node_count), int64) .or. & + count_value < 1_int64) then + call mixed_encoding_error(status) + return + end if + node_count = int(count_value) + if ((topology == XDMF_TOPOLOGY_POLYLINE .and. node_count < 2) .or. & + (topology == XDMF_TOPOLOGY_POLYGON .and. node_count < 3)) then + call mixed_encoding_error(status) + return + end if + position = position + 1 + call convert_mixed_nodes(input, output, position, node_count, & + number_of_points, status) + if (status%is_error()) return + + case (XDMF_TOPOLOGY_POLYHEDRON) + if (position > size(input)) then + call mixed_encoding_error(status) + return + end if + if (input(position) < 1_int64 .or. & + input(position) > int(huge(face_count), int64)) then + call mixed_encoding_error(status) + return + end if + face_count = int(input(position)) + position = position + 1 + do face = 1, face_count + if (position > size(input)) then + call mixed_encoding_error(status) + return + end if + if (input(position) < 3_int64 .or. & + input(position) > int(huge(node_count), int64)) then + call mixed_encoding_error(status) + return + end if + node_count = int(input(position)) + position = position + 1 + call convert_mixed_nodes(input, output, position, node_count, & + number_of_points, status) + if (status%is_error()) return + end do + + case default + if (.not. topology_is_supported(topology) .or. & + is_structured_topology(topology) .or. & + topology == XDMF_TOPOLOGY_MIXED) then + call mixed_encoding_error(status) + return + end if + node_count = topology_nodes_per_element(topology) + if (node_count <= 0) then + call mixed_encoding_error(status) + return + end if + call convert_mixed_nodes(input, output, position, node_count, & + number_of_points, status) + if (status%is_error()) return + end select + number_of_elements = number_of_elements + 1_int64 + end do + end subroutine convert_mixed_connectivity + + subroutine convert_mixed_nodes(input, output, position, node_count, & + number_of_points, status) + integer(int64), intent(in) :: input(:) + integer(int64), intent(inout) :: output(:) + integer, intent(inout) :: position + integer, intent(in) :: node_count + integer(int64), intent(in) :: number_of_points + type(xdmf_status_t), intent(out) :: status + + integer :: last + + call set_status_success(status) + if (node_count > size(input) - position + 1) then + call mixed_encoding_error(status) + return + end if + last = position + node_count - 1 + if (any(input(position:last) < 1_int64) .or. & + any(input(position:last) > number_of_points)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Mixed connectivity contains a node index outside the point range') + return + end if + output(position:last) = input(position:last) - 1_int64 + position = last + 1 + end subroutine convert_mixed_nodes + + subroutine mixed_encoding_error(status) + type(xdmf_status_t), intent(out) :: status + + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Invalid or truncated standard XDMF mixed connectivity encoding') + end subroutine mixed_encoding_error + + subroutine validate_node_indices(connectivity, number_of_points, status) + integer(int64), intent(in) :: connectivity(:, :) + integer(int64), intent(in) :: number_of_points + type(xdmf_status_t), intent(out) :: status + + if (any(connectivity < 1_int64) .or. & + any(connectivity > number_of_points)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Connectivity contains a node index outside the point range') + else + call set_status_success(status) + end if + end subroutine validate_node_indices + + subroutine validate_options(options, status) + type(xdmf_options_t), intent(in) :: options + type(xdmf_status_t), intent(out) :: status + + integer :: mpi_error, rank_count + logical :: mpi_is_initialized + + select case (options%series_kind) + case (XDMF_SERIES_NONE, XDMF_SERIES_TIME, XDMF_SERIES_FREQUENCY, & + XDMF_SERIES_PARAMETER) + case default + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Invalid series kind') + return + end select + if (options%compression_level < 0 .or. options%compression_level > 9) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'The HDF5 compression level must be between 0 and 9') + return + end if + if (options%chunk_target_bytes <= 0_int64) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'The HDF5 chunk target must be positive') + return + end if + if (options%collective_io .and. options%compression_level /= 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Collective HDF5 output does not currently support compression') + return + end if + if (options%collective_io) then +#ifdef XDMF_HDF5_WITH_MPI + call MPI_Initialized(mpi_is_initialized, mpi_error) + if (mpi_error /= MPI_SUCCESS .or. .not. mpi_is_initialized .or. & + options%communicator == MPI_COMM_NULL) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Collective HDF5 output requires an initialized MPI communicator') + return + end if + call MPI_Comm_size(options%communicator, rank_count, mpi_error) + if (mpi_error /= MPI_SUCCESS .or. options%root_rank < 0 .or. & + options%root_rank >= rank_count) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'The collective-output root rank is outside the communicator') + return + end if +#ifdef XDMF_HDF5_WITH_PARALLEL_HDF5 +#else + call set_status_error(status, XDMF_ERROR_STATE, & + 'This library was built with MPI but the selected HDF5 is serial') + return +#endif +#else + call set_status_error(status, XDMF_ERROR_STATE, & + 'This library was built without MPI support') + return +#endif + end if + call set_status_success(status) + end subroutine validate_options + + subroutine check_definition_state(this, name, status) + class(xdmf_writer_t), intent(in) :: this + character(len=*), intent(in) :: name + type(xdmf_status_t), intent(out) :: status + + call check_open(this, status) + if (status%is_error()) return + if (this%definitions_locked) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Definitions are locked after the first series step begins') + return + end if + if (len_trim(name) == 0) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Definition names must not be empty') + end if + end subroutine check_definition_state + + subroutine check_open(this, status) + class(xdmf_writer_t), intent(in) :: this + type(xdmf_status_t), intent(out) :: status + + if (this%is_open .and. .not. this%is_poisoned) then + call set_status_success(status) + else if (this%is_poisoned) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'The XDMF writer is poisoned and must be closed') + else + call set_status_error(status, XDMF_ERROR_STATE, & + 'The XDMF writer is not open') + end if + end subroutine check_open + + subroutine synchronize_collective_status(this, status, context) + class(xdmf_writer_t), intent(in) :: this + type(xdmf_status_t), intent(inout) :: status + character(len=*), intent(in) :: context + +#ifdef XDMF_HDF5_WITH_MPI + integer :: local_code, global_code, mpi_error +#endif + + if (.not. this%is_collective) return +#ifdef XDMF_HDF5_WITH_MPI + local_code = status%error_code() + call MPI_Allreduce(local_code, global_code, 1, MPI_INTEGER, MPI_MAX, & + this%communicator, mpi_error) + if (mpi_error /= MPI_SUCCESS) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Could not synchronize collective writer status') + else if (global_code /= XDMF_SUCCESS .and. local_code == XDMF_SUCCESS) then + call set_status_error(status, global_code, context) + end if +#else + call set_status_error(status, XDMF_ERROR_STATE, & + 'This library was built without parallel HDF5 support') +#endif + end subroutine synchronize_collective_status + + logical function collection_exists(this, id) + class(xdmf_writer_t), intent(in) :: this + integer, intent(in) :: id + + integer :: index + + collection_exists = .false. + do index = 1, size(this%collections) + if (this%collections(index)%id == id) then + collection_exists = .true. + return + end if + end do + end function collection_exists + + integer function find_grid(this, id) + class(xdmf_writer_t), intent(in) :: this + type(xdmf_grid_id_t), intent(in) :: id + + integer :: index, value + + find_grid = 0 + if (grid_id_owner(id) /= this%owner_token) return + value = grid_id_value(id) + do index = 1, size(this%grids) + if (this%grids(index)%id == value) then + find_grid = index + return + end if + end do + end function find_grid + + integer function find_attribute(this, id) + class(xdmf_writer_t), intent(in) :: this + type(xdmf_attribute_id_t), intent(in) :: id + + integer :: index, value + + find_attribute = 0 + if (attribute_id_owner(id) /= this%owner_token) return + value = attribute_id_value(id) + do index = 1, size(this%attributes) + if (this%attributes(index)%id == value) then + find_attribute = index + return + end if + end do + end function find_attribute + + logical function is_structured_topology(topology) + integer, intent(in) :: topology + + select case (topology) + case (XDMF_TOPOLOGY_2D_SMESH, XDMF_TOPOLOGY_2D_RECTMESH, & + XDMF_TOPOLOGY_2D_CORECTMESH, XDMF_TOPOLOGY_3D_SMESH, & + XDMF_TOPOLOGY_3D_RECTMESH, XDMF_TOPOLOGY_3D_CORECTMESH) + is_structured_topology = .true. + case default + is_structured_topology = .false. + end select + end function is_structured_topology + + subroutine append_collection(this, record, status) + class(xdmf_writer_t), intent(inout) :: this + type(collection_record_t), intent(in) :: record + type(xdmf_status_t), intent(out) :: status + + type(collection_record_t), allocatable :: records(:) + integer :: allocation_status, count + + count = size(this%collections) + allocate(records(count + 1), stat=allocation_status) + if (allocation_status /= 0) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Could not store collection metadata') + return + end if + if (count > 0) records(:count) = this%collections + records(count + 1) = record + call move_alloc(records, this%collections) + call set_status_success(status) + end subroutine append_collection + + subroutine append_grid(this, record, status) + class(xdmf_writer_t), intent(inout) :: this + type(grid_record_t), intent(in) :: record + type(xdmf_status_t), intent(out) :: status + + type(grid_record_t), allocatable :: records(:) + integer :: allocation_status, count + + count = size(this%grids) + allocate(records(count + 1), stat=allocation_status) + if (allocation_status /= 0) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Could not store grid metadata') + return + end if + if (count > 0) records(:count) = this%grids + records(count + 1) = record + call move_alloc(records, this%grids) + call set_status_success(status) + end subroutine append_grid + + subroutine append_attribute(this, record, status) + class(xdmf_writer_t), intent(inout) :: this + type(attribute_record_t), intent(in) :: record + type(xdmf_status_t), intent(out) :: status + + type(attribute_record_t), allocatable :: records(:) + integer :: allocation_status, count + + count = size(this%attributes) + allocate(records(count + 1), stat=allocation_status) + if (allocation_status /= 0) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Could not store attribute metadata') + return + end if + if (count > 0) records(:count) = this%attributes + records(count + 1) = record + call move_alloc(records, this%attributes) + call set_status_success(status) + end subroutine append_attribute + + subroutine reset_writer_metadata(this) + class(xdmf_writer_t), intent(inout) :: this + + if (allocated(this%collections)) deallocate(this%collections) + if (allocated(this%grids)) deallocate(this%grids) + if (allocated(this%attributes)) deallocate(this%attributes) + if (allocated(this%series_values)) deallocate(this%series_values) + allocate(this%collections(0), this%grids(0), this%attributes(0)) + allocate(this%series_values(0)) + this%next_collection_id = 1 + this%next_grid_id = 1 + this%next_attribute_id = 1 + this%committed_steps = 0 + this%owner_token = next_writer_token + if (next_writer_token == huge(next_writer_token)) then + next_writer_token = 1_int64 + else + next_writer_token = next_writer_token + 1_int64 + end if + this%active_step_value = 0.0_real64 + this%communicator = 0 + this%rank = 0 + this%root_rank = 0 + this%definitions_locked = .false. + this%step_is_active = .false. + this%is_poisoned = .false. + this%is_collective = .false. + end subroutine reset_writer_metadata + + subroutine close_after_create_failure(this, status) + class(xdmf_writer_t), intent(inout) :: this + type(xdmf_status_t), intent(inout) :: status + + type(xdmf_status_t) :: original_status, close_status + + original_status = status + call hdf_close_file(this%hdf5_file, close_status) + if (close_status%is_error()) then + this%is_open = .true. + this%is_poisoned = .true. + status = close_status + else + this%is_open = .false. + status = original_status + end if + end subroutine close_after_create_failure + + subroutine derive_output_paths(path, xdmf_path, hdf5_path, hdf5_name) + character(len=*), intent(in) :: path + character(len=:), allocatable, intent(out) :: xdmf_path + character(len=:), allocatable, intent(out) :: hdf5_path + character(len=:), allocatable, intent(out) :: hdf5_name + + character(len=:), allocatable :: trimmed, stem + integer :: separator + + trimmed = trim(path) + if (ends_with(trimmed, '.xdmf')) then + xdmf_path = trimmed + stem = trimmed(:len(trimmed) - len('.xdmf')) + hdf5_path = stem//'.h5' + else if (ends_with(trimmed, '.xmf')) then + xdmf_path = trimmed + stem = trimmed(:len(trimmed) - len('.xmf')) + hdf5_path = stem//'.h5' + else if (ends_with(trimmed, '.hdf5')) then + hdf5_path = trimmed + stem = trimmed(:len(trimmed) - len('.hdf5')) + xdmf_path = stem//'.xdmf' + else if (ends_with(trimmed, '.h5')) then + hdf5_path = trimmed + stem = trimmed(:len(trimmed) - len('.h5')) + xdmf_path = stem//'.xdmf' + else + xdmf_path = trimmed//'.xdmf' + hdf5_path = trimmed//'.h5' + end if + + separator = max(index(hdf5_path, '/', back=.true.), & + index(hdf5_path, achar(92), back=.true.)) + hdf5_name = hdf5_path(separator + 1:) + end subroutine derive_output_paths + + logical function ends_with(value, suffix) + character(len=*), intent(in) :: value, suffix + + if (len(value) < len(suffix)) then + ends_with = .false. + else + ends_with = value(len(value) - len(suffix) + 1:) == suffix + end if + end function ends_with + + function indexed_path(prefix, id) result(path) + character(len=*), intent(in) :: prefix + integer, intent(in) :: id + character(len=:), allocatable :: path + + character(len=32) :: number + + if (id < 10000) then + write(number, '(I4.4)') id + else + write(number, '(I0)') id + end if + path = prefix//trim(number) + end function indexed_path + +end module xdmf_hdf5_m diff --git a/external/xdmf-hdf5/src/xdmf_hdf5_backend.F90 b/external/xdmf-hdf5/src/xdmf_hdf5_backend.F90 new file mode 100644 index 000000000..4e506edc2 --- /dev/null +++ b/external/xdmf-hdf5/src/xdmf_hdf5_backend.F90 @@ -0,0 +1,1196 @@ +module xdmf_hdf5_backend_m + use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64 + use hdf5 +#ifdef XDMF_HDF5_WITH_PARALLEL_HDF5 + use mpi +#endif + use xdmf_model_m, only: xdmf_status_t, xdmf_options_t, & + XDMF_ERROR_ARGUMENT, XDMF_ERROR_HDF5, XDMF_ERROR_CONSISTENCY, & + XDMF_NUMERIC_REAL32, XDMF_NUMERIC_REAL64, & + XDMF_NUMERIC_INT32, XDMF_NUMERIC_INT64, & + numeric_type_size, product_int64, set_status_success, set_status_error + + implicit none + + private + + type, public :: hdf5_file_t + private + integer(HID_T) :: id = -1_HID_T + integer(HID_T) :: transfer_property = -1_HID_T + integer :: communicator = 0 + integer :: rank = 0 + integer :: root_rank = 0 + logical :: collective = .false. + end type hdf5_file_t + + public :: hdf_create_file + public :: hdf_file_is_open + public :: hdf_close_file + public :: hdf_flush_file + public :: hdf_create_group + public :: hdf_write_dataset + public :: hdf_create_series_dataset + public :: hdf_append_series + public :: hdf_append_series_hyperslab + public :: hdf_truncate_series + + interface hdf_write_dataset + module procedure hdf_write_dataset_r4 + module procedure hdf_write_dataset_r8 + module procedure hdf_write_dataset_i4 + module procedure hdf_write_dataset_i8 + end interface hdf_write_dataset + + interface hdf_append_series + module procedure hdf_append_series_r4 + module procedure hdf_append_series_r8 + module procedure hdf_append_series_i4 + module procedure hdf_append_series_i8 + end interface hdf_append_series + + interface hdf_append_series_hyperslab + module procedure hdf_append_series_hyperslab_r4 + module procedure hdf_append_series_hyperslab_r8 + module procedure hdf_append_series_hyperslab_i4 + module procedure hdf_append_series_hyperslab_i8 + end interface hdf_append_series_hyperslab + + logical :: hdf5_initialized = .false. + +contains + + subroutine hdf_create_file(file, path, options, status) + type(hdf5_file_t), intent(out) :: file + character(len=*), intent(in) :: path + type(xdmf_options_t), intent(in) :: options + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: access_property + integer :: access_flag, close_error, error +#ifdef XDMF_HDF5_WITH_PARALLEL_HDF5 + integer :: mpi_error, rank_count +#endif + + call set_status_success(status) + file%transfer_property = H5P_DEFAULT_F +#ifdef XDMF_HDF5_WITH_PARALLEL_HDF5 + rank_count = 0 +#endif + if (.not. hdf5_initialized) then + call h5open_f(error) + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not initialize the HDF5 Fortran interface') + return + end if + hdf5_initialized = .true. + else + error = 0 + end if + access_property = -1_HID_T + call h5pcreate_f(H5P_FILE_ACCESS_F, access_property, error) + if (error == 0) then + call h5pset_fclose_degree_f(access_property, H5F_CLOSE_STRONG_F, error) + end if + if (error == 0 .and. options%collective_io) then +#ifdef XDMF_HDF5_WITH_PARALLEL_HDF5 + file%collective = .true. + file%communicator = options%communicator + file%root_rank = options%root_rank + call MPI_Comm_rank(file%communicator, file%rank, mpi_error) + if (mpi_error == MPI_SUCCESS) then + call MPI_Comm_size(file%communicator, rank_count, mpi_error) + end if + if (mpi_error /= MPI_SUCCESS .or. file%root_rank < 0 .or. & + file%root_rank >= rank_count) then + error = -1 + else + call h5pset_fapl_mpio_f(access_property, file%communicator, & + MPI_INFO_NULL, error) + end if + if (error == 0) then + call h5pcreate_f(H5P_DATASET_XFER_F, file%transfer_property, error) + end if + if (error == 0) then + call h5pset_dxpl_mpio_f(file%transfer_property, & + H5FD_MPIO_COLLECTIVE_F, error) + end if +#else + error = -1 +#endif + end if + if (error /= 0) then + if (access_property >= 0_HID_T) then + call h5pclose_f(access_property, close_error) + end if + if (file%transfer_property /= H5P_DEFAULT_F) then + call h5pclose_f(file%transfer_property, close_error) + file%transfer_property = H5P_DEFAULT_F + end if + if (options%collective_io) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'Collective HDF5 output is unavailable or has invalid MPI options') + else + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not configure HDF5 file ownership') + end if + return + end if + if (options%overwrite) then + access_flag = H5F_ACC_TRUNC_F + else + access_flag = H5F_ACC_EXCL_F + end if + call h5fcreate_f(trim(path), access_flag, file%id, error, & + H5P_DEFAULT_F, access_property) + call h5pclose_f(access_property, close_error) + if (error /= 0 .or. file%id < 0_HID_T) then + if (file%transfer_property /= H5P_DEFAULT_F) then + call h5pclose_f(file%transfer_property, close_error) + file%transfer_property = H5P_DEFAULT_F + end if + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not create HDF5 file: '//trim(path)) + return + end if + if (close_error /= 0) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Could not close the HDF5 file access property list') + call close_after_create_error(file, status) + return + end if + + call write_string_attribute(file%id, 'schema_name', & + 'XDMF-HDF5', status) + if (status%is_error()) then + call close_after_create_error(file, status) + return + end if + call write_string_attribute(file%id, 'schema_version', '1.0', status) + if (status%is_error()) then + call close_after_create_error(file, status) + return + end if + end subroutine hdf_create_file + + logical function hdf_file_is_open(file) + type(hdf5_file_t), intent(in) :: file + + hdf_file_is_open = file%id >= 0_HID_T + end function hdf_file_is_open + + subroutine hdf_close_file(file, status) + type(hdf5_file_t), intent(inout) :: file + type(xdmf_status_t), intent(out) :: status + + integer :: error, property_error + + call set_status_success(status) + property_error = 0 + if (file%id >= 0_HID_T) then + call h5fclose_f(file%id, error) + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not close the HDF5 file') + else + file%id = -1_HID_T + end if + end if + if (file%transfer_property /= H5P_DEFAULT_F) then + call h5pclose_f(file%transfer_property, property_error) + if (property_error == 0) file%transfer_property = H5P_DEFAULT_F + end if + if (property_error /= 0 .and. .not. status%is_error()) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Could not close the collective HDF5 transfer property list') + end if + end subroutine hdf_close_file + + subroutine hdf_flush_file(file, status) + type(hdf5_file_t), intent(in) :: file + type(xdmf_status_t), intent(out) :: status + + integer :: error + + call set_status_success(status) + if (file%id < 0_HID_T) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Cannot flush a closed HDF5 file') + return + end if + + call h5fflush_f(file%id, H5F_SCOPE_GLOBAL_F, error) + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not flush the HDF5 file') + end if + end subroutine hdf_flush_file + + subroutine hdf_create_group(file, path, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: group_id + integer :: error + + call set_status_success(status) + call h5gcreate_f(file%id, trim(path), group_id, error) + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not create HDF5 group: '//trim(path)) + return + end if + + call h5gclose_f(group_id, error) + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not close HDF5 group: '//trim(path)) + end if + end subroutine hdf_create_group + + subroutine hdf_write_dataset_r4(file, path, data, shape, status, & + local_offset, local_count) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + real(real32), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + type(xdmf_status_t), intent(out) :: status + integer(int64), intent(in), optional :: local_offset(:), local_count(:) + + real(real32) :: dummy(1) + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T) :: buffer_dims(1) + integer :: error + + call create_fixed_dataset(file, path, shape, & + hdf_datatype(XDMF_NUMERIC_REAL32), & + dataset_id, status) + if (status%is_error()) return + + dummy = 0.0_real32 + buffer_dims(1) = int(max(1, size(data)), HSIZE_T) + call prepare_full_transfer(file, dataset_id, buffer_dims, filespace, & + memspace, status, local_offset, local_count) + if (status%is_error()) then + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + -1, status) + return + end if + if (size(data) == 0) then + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL32), & + dummy, buffer_dims, error, memspace, filespace, file%transfer_property) + else + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL32), & + data, buffer_dims, error, memspace, filespace, file%transfer_property) + end if + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + error, status) + end subroutine hdf_write_dataset_r4 + + subroutine hdf_write_dataset_r8(file, path, data, shape, status, & + local_offset, local_count) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + real(real64), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + type(xdmf_status_t), intent(out) :: status + integer(int64), intent(in), optional :: local_offset(:), local_count(:) + + real(real64) :: dummy(1) + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T) :: buffer_dims(1) + integer :: error + + call create_fixed_dataset(file, path, shape, & + hdf_datatype(XDMF_NUMERIC_REAL64), & + dataset_id, status) + if (status%is_error()) return + + dummy = 0.0_real64 + buffer_dims(1) = int(max(1, size(data)), HSIZE_T) + call prepare_full_transfer(file, dataset_id, buffer_dims, filespace, & + memspace, status, local_offset, local_count) + if (status%is_error()) then + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + -1, status) + return + end if + if (size(data) == 0) then + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL64), & + dummy, buffer_dims, error, memspace, filespace, file%transfer_property) + else + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL64), & + data, buffer_dims, error, memspace, filespace, file%transfer_property) + end if + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + error, status) + end subroutine hdf_write_dataset_r8 + + subroutine hdf_write_dataset_i4(file, path, data, shape, status, & + local_offset, local_count) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int32), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + type(xdmf_status_t), intent(out) :: status + integer(int64), intent(in), optional :: local_offset(:), local_count(:) + + integer(int32) :: dummy(1) + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T) :: buffer_dims(1) + integer :: error + + call create_fixed_dataset(file, path, shape, & + hdf_datatype(XDMF_NUMERIC_INT32), & + dataset_id, status) + if (status%is_error()) return + + dummy = 0_int32 + buffer_dims(1) = int(max(1, size(data)), HSIZE_T) + call prepare_full_transfer(file, dataset_id, buffer_dims, filespace, & + memspace, status, local_offset, local_count) + if (status%is_error()) then + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + -1, status) + return + end if + if (size(data) == 0) then + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT32), & + dummy, buffer_dims, error, memspace, filespace, file%transfer_property) + else + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT32), & + data, buffer_dims, error, memspace, filespace, file%transfer_property) + end if + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + error, status) + end subroutine hdf_write_dataset_i4 + + subroutine hdf_write_dataset_i8(file, path, data, shape, status, & + local_offset, local_count) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int64), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + type(xdmf_status_t), intent(out) :: status + integer(int64), intent(in), optional :: local_offset(:), local_count(:) + + integer(int64) :: dummy(1) + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T) :: buffer_dims(1) + integer :: error + + call create_fixed_dataset(file, path, shape, & + hdf_datatype(XDMF_NUMERIC_INT64), & + dataset_id, status) + if (status%is_error()) return + + dummy = 0_int64 + buffer_dims(1) = int(max(1, size(data)), HSIZE_T) + call prepare_full_transfer(file, dataset_id, buffer_dims, filespace, & + memspace, status, local_offset, local_count) + if (status%is_error()) then + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + -1, status) + return + end if + if (size(data) == 0) then + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT64), & + dummy, buffer_dims, error, memspace, filespace, file%transfer_property) + else + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT64), & + data, buffer_dims, error, memspace, filespace, file%transfer_property) + end if + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + error, status) + end subroutine hdf_write_dataset_i8 + + subroutine hdf_create_series_dataset(file, path, numeric_type, shape, & + options, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer, intent(in) :: numeric_type + integer(int64), intent(in) :: shape(:) + type(xdmf_options_t), intent(in) :: options + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: dataspace_id, dataset_id, property_id, datatype + integer(HSIZE_T), allocatable :: dims(:), maxdims(:), chunks(:) + integer :: close_error, error, rank + + call set_status_success(status) + dataspace_id = -1_HID_T + dataset_id = -1_HID_T + property_id = -1_HID_T + rank = size(shape) + 1 + allocate(dims(rank), maxdims(rank), chunks(rank)) + if (size(shape) > 0) then + dims(:rank - 1) = int(shape, HSIZE_T) + maxdims(:rank - 1) = int(shape, HSIZE_T) + call choose_chunk_shape(shape, numeric_type, & + options%chunk_target_bytes, chunks(:rank - 1)) + chunks(rank) = 1_HSIZE_T + else + chunks(rank) = 256_HSIZE_T + end if + dims(rank) = 0_HSIZE_T + maxdims(rank) = H5S_UNLIMITED_F + + call h5screate_simple_f(rank, dims, dataspace_id, error, maxdims) + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not create series dataspace: '//trim(path)) + return + end if + + call h5pcreate_f(H5P_DATASET_CREATE_F, property_id, error) + if (error == 0) call h5pset_chunk_f(property_id, rank, chunks, error) + if (error == 0 .and. options%compression_level > 0) then + call h5pset_deflate_f(property_id, options%compression_level, error) + end if + if (error /= 0) then + if (property_id >= 0_HID_T) then + call h5pclose_f(property_id, close_error) + end if + call h5sclose_f(dataspace_id, close_error) + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not configure series dataset: '//trim(path)) + return + end if + + datatype = hdf_datatype(numeric_type) + call h5dcreate_f(file%id, trim(path), datatype, dataspace_id, & + dataset_id, error, property_id) + if (error /= 0) then + call h5pclose_f(property_id, close_error) + call h5sclose_f(dataspace_id, close_error) + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not create series dataset: '//trim(path)) + return + end if + + call h5dclose_f(dataset_id, error) + call h5pclose_f(property_id, close_error) + if (error == 0) error = close_error + call h5sclose_f(dataspace_id, close_error) + if (error == 0) error = close_error + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Could not close series dataset resources: '//trim(path)) + end if + end subroutine hdf_create_series_dataset + + subroutine hdf_append_series_r4(file, path, data, shape, committed_steps, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + real(real32), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + type(xdmf_status_t), intent(out) :: status + + call append_series_r4_impl(file, path, data, shape, committed_steps, & + hdf_datatype(XDMF_NUMERIC_REAL32), status) + end subroutine hdf_append_series_r4 + + subroutine hdf_append_series_r8(file, path, data, shape, committed_steps, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + real(real64), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + type(xdmf_status_t), intent(out) :: status + + call append_series_r8_impl(file, path, data, shape, committed_steps, & + hdf_datatype(XDMF_NUMERIC_REAL64), status) + end subroutine hdf_append_series_r8 + + subroutine hdf_append_series_i4(file, path, data, shape, committed_steps, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int32), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + type(xdmf_status_t), intent(out) :: status + + call append_series_i4_impl(file, path, data, shape, committed_steps, & + hdf_datatype(XDMF_NUMERIC_INT32), status) + end subroutine hdf_append_series_i4 + + subroutine hdf_append_series_i8(file, path, data, shape, committed_steps, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int64), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + type(xdmf_status_t), intent(out) :: status + + call append_series_i8_impl(file, path, data, shape, committed_steps, & + hdf_datatype(XDMF_NUMERIC_INT64), status) + end subroutine hdf_append_series_i8 + + subroutine hdf_append_series_hyperslab_r4(file, path, data, shape, offset, & + count, committed_steps, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + real(real32), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:), offset(:), count(:) + integer, intent(in) :: committed_steps + type(xdmf_status_t), intent(out) :: status + + real(real32) :: dummy(1) + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T), allocatable :: new_dims(:), hdf_offset(:), hdf_count(:) + integer(HSIZE_T) :: mem_dims(1) + integer :: error + + dummy = 0.0_real32 + call prepare_append(file, path, shape, committed_steps, dataset_id, & + filespace, memspace, new_dims, hdf_offset, hdf_count, mem_dims, status, & + offset, count) + if (status%is_error()) return + if (size(data) == 0) then + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL32), dummy, & + mem_dims, error, memspace, filespace, file%transfer_property) + else + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL32), data, & + mem_dims, error, memspace, filespace, file%transfer_property) + end if + call finish_append(dataset_id, filespace, memspace, path, shape, & + committed_steps, error, status) + end subroutine hdf_append_series_hyperslab_r4 + + subroutine hdf_append_series_hyperslab_r8(file, path, data, shape, offset, & + count, committed_steps, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + real(real64), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:), offset(:), count(:) + integer, intent(in) :: committed_steps + type(xdmf_status_t), intent(out) :: status + + real(real64) :: dummy(1) + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T), allocatable :: new_dims(:), hdf_offset(:), hdf_count(:) + integer(HSIZE_T) :: mem_dims(1) + integer :: error + + dummy = 0.0_real64 + call prepare_append(file, path, shape, committed_steps, dataset_id, & + filespace, memspace, new_dims, hdf_offset, hdf_count, mem_dims, status, & + offset, count) + if (status%is_error()) return + if (size(data) == 0) then + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL64), dummy, & + mem_dims, error, memspace, filespace, file%transfer_property) + else + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL64), data, & + mem_dims, error, memspace, filespace, file%transfer_property) + end if + call finish_append(dataset_id, filespace, memspace, path, shape, & + committed_steps, error, status) + end subroutine hdf_append_series_hyperslab_r8 + + subroutine hdf_append_series_hyperslab_i4(file, path, data, shape, offset, & + count, committed_steps, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int32), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:), offset(:), count(:) + integer, intent(in) :: committed_steps + type(xdmf_status_t), intent(out) :: status + + integer(int32) :: dummy(1) + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T), allocatable :: new_dims(:), hdf_offset(:), hdf_count(:) + integer(HSIZE_T) :: mem_dims(1) + integer :: error + + dummy = 0_int32 + call prepare_append(file, path, shape, committed_steps, dataset_id, & + filespace, memspace, new_dims, hdf_offset, hdf_count, mem_dims, status, & + offset, count) + if (status%is_error()) return + if (size(data) == 0) then + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT32), dummy, & + mem_dims, error, memspace, filespace, file%transfer_property) + else + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT32), data, & + mem_dims, error, memspace, filespace, file%transfer_property) + end if + call finish_append(dataset_id, filespace, memspace, path, shape, & + committed_steps, error, status) + end subroutine hdf_append_series_hyperslab_i4 + + subroutine hdf_append_series_hyperslab_i8(file, path, data, shape, offset, & + count, committed_steps, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int64), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:), offset(:), count(:) + integer, intent(in) :: committed_steps + type(xdmf_status_t), intent(out) :: status + + integer(int64) :: dummy(1) + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T), allocatable :: new_dims(:), hdf_offset(:), hdf_count(:) + integer(HSIZE_T) :: mem_dims(1) + integer :: error + + dummy = 0_int64 + call prepare_append(file, path, shape, committed_steps, dataset_id, & + filespace, memspace, new_dims, hdf_offset, hdf_count, mem_dims, status, & + offset, count) + if (status%is_error()) return + if (size(data) == 0) then + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT64), dummy, & + mem_dims, error, memspace, filespace, file%transfer_property) + else + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT64), data, & + mem_dims, error, memspace, filespace, file%transfer_property) + end if + call finish_append(dataset_id, filespace, memspace, path, shape, & + committed_steps, error, status) + end subroutine hdf_append_series_hyperslab_i8 + + subroutine append_series_r4_impl(file, path, data, shape, committed_steps, & + datatype, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + real(real32), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + integer(HID_T), intent(in) :: datatype + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T), allocatable :: new_dims(:), offset(:), count(:) + integer(HSIZE_T) :: mem_dims(1) + integer :: error + + call prepare_append(file, path, shape, committed_steps, dataset_id, & + filespace, memspace, new_dims, offset, count, mem_dims, status) + if (status%is_error()) return + call h5dwrite_f(dataset_id, datatype, data, mem_dims, error, & + memspace, filespace, file%transfer_property) + call finish_append(dataset_id, filespace, memspace, path, shape, & + committed_steps, error, status) + end subroutine append_series_r4_impl + + subroutine append_series_r8_impl(file, path, data, shape, committed_steps, & + datatype, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + real(real64), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + integer(HID_T), intent(in) :: datatype + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T), allocatable :: new_dims(:), offset(:), count(:) + integer(HSIZE_T) :: mem_dims(1) + integer :: error + + call prepare_append(file, path, shape, committed_steps, dataset_id, & + filespace, memspace, new_dims, offset, count, mem_dims, status) + if (status%is_error()) return + call h5dwrite_f(dataset_id, datatype, data, mem_dims, error, & + memspace, filespace, file%transfer_property) + call finish_append(dataset_id, filespace, memspace, path, shape, & + committed_steps, error, status) + end subroutine append_series_r8_impl + + subroutine append_series_i4_impl(file, path, data, shape, committed_steps, & + datatype, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int32), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + integer(HID_T), intent(in) :: datatype + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T), allocatable :: new_dims(:), offset(:), count(:) + integer(HSIZE_T) :: mem_dims(1) + integer :: error + + call prepare_append(file, path, shape, committed_steps, dataset_id, & + filespace, memspace, new_dims, offset, count, mem_dims, status) + if (status%is_error()) return + call h5dwrite_f(dataset_id, datatype, data, mem_dims, error, & + memspace, filespace, file%transfer_property) + call finish_append(dataset_id, filespace, memspace, path, shape, & + committed_steps, error, status) + end subroutine append_series_i4_impl + + subroutine append_series_i8_impl(file, path, data, shape, committed_steps, & + datatype, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int64), intent(in) :: data(:) + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + integer(HID_T), intent(in) :: datatype + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: dataset_id, filespace, memspace + integer(HSIZE_T), allocatable :: new_dims(:), offset(:), count(:) + integer(HSIZE_T) :: mem_dims(1) + integer :: error + + call prepare_append(file, path, shape, committed_steps, dataset_id, & + filespace, memspace, new_dims, offset, count, mem_dims, status) + if (status%is_error()) return + call h5dwrite_f(dataset_id, datatype, data, mem_dims, error, & + memspace, filespace, file%transfer_property) + call finish_append(dataset_id, filespace, memspace, path, shape, & + committed_steps, error, status) + end subroutine append_series_i8_impl + + subroutine hdf_truncate_series(file, path, shape, committed_steps, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: dataset_id + integer(HSIZE_T), allocatable :: dims(:) + integer :: error, close_error, rank + + call set_status_success(status) + dataset_id = -1_HID_T + close_error = 0 + rank = size(shape) + 1 + allocate(dims(rank)) + if (size(shape) > 0) dims(:rank - 1) = int(shape, HSIZE_T) + dims(rank) = int(committed_steps, HSIZE_T) + + call h5dopen_f(file%id, trim(path), dataset_id, error) + if (error == 0) call h5dset_extent_f(dataset_id, dims, error) + if (dataset_id >= 0_HID_T) call h5dclose_f(dataset_id, close_error) + if (close_error /= 0) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Rollback succeeded but dataset close failed: '//trim(path)) + else if (error /= 0) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not roll back series dataset: '//trim(path)) + end if + end subroutine hdf_truncate_series + + subroutine create_fixed_dataset(file, path, shape, datatype, dataset_id, status) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int64), intent(in) :: shape(:) + integer(HID_T), intent(in) :: datatype + integer(HID_T), intent(out) :: dataset_id + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: dataspace_id + integer(HSIZE_T), allocatable :: dims(:) + integer :: dataset_close_error, delete_error, error, close_error + + call set_status_success(status) + dataset_id = -1_HID_T + dataspace_id = -1_HID_T + close_error = 0 + if (size(shape) == 0 .or. any(shape <= 0_int64)) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'A fixed HDF5 dataset must have positive dimensions: '//trim(path)) + return + end if + + allocate(dims(size(shape))) + dims = int(shape, HSIZE_T) + call h5screate_simple_f(size(shape), dims, dataspace_id, error) + if (error == 0) then + call h5dcreate_f(file%id, trim(path), datatype, dataspace_id, & + dataset_id, error) + end if + if (dataspace_id >= 0_HID_T) call h5sclose_f(dataspace_id, close_error) + if (close_error /= 0) then + dataset_close_error = 0 + delete_error = 0 + if (dataset_id >= 0_HID_T) then + call h5dclose_f(dataset_id, dataset_close_error) + if (dataset_close_error == 0) then + dataset_id = -1_HID_T + call h5ldelete_f(file%id, trim(path), delete_error) + end if + end if + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Could not close fixed dataset resources: '//trim(path)) + return + end if + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not create HDF5 dataset: '//trim(path)) + end if + end subroutine create_fixed_dataset + + subroutine prepare_full_transfer(file, dataset_id, buffer_dims, filespace, & + memspace, status, local_offset, local_count) + type(hdf5_file_t), intent(in) :: file + integer(HID_T), intent(in) :: dataset_id + integer(HSIZE_T), intent(in) :: buffer_dims(1) + integer(HID_T), intent(out) :: filespace, memspace + type(xdmf_status_t), intent(out) :: status + integer(int64), intent(in), optional :: local_offset(:), local_count(:) + + integer(HSIZE_T), allocatable :: hdf_offset(:), hdf_count(:) + integer :: error, close_error + + call set_status_success(status) + filespace = -1_HID_T + memspace = -1_HID_T + call h5dget_space_f(dataset_id, filespace, error) + if (error == 0) call h5screate_simple_f(1, buffer_dims, memspace, error) + if (error == 0 .and. present(local_offset) .neqv. present(local_count)) then + error = -1 + else if (error == 0 .and. present(local_offset)) then + if (size(local_offset) /= size(local_count)) then + error = -1 + else if (all(local_count == 0_int64)) then + call h5sselect_none_f(filespace, error) + if (error == 0) call h5sselect_none_f(memspace, error) + else + allocate (hdf_offset(size(local_offset)), hdf_count(size(local_count))) + hdf_offset = int(local_offset, HSIZE_T) + hdf_count = int(local_count, HSIZE_T) + call h5sselect_hyperslab_f(filespace, H5S_SELECT_SET_F, hdf_offset, & + hdf_count, error) + end if + else if (error == 0 .and. file%collective .and. & + file%rank /= file%root_rank) then + call h5sselect_none_f(filespace, error) + if (error == 0) call h5sselect_none_f(memspace, error) + end if + if (error /= 0) then + if (memspace >= 0_HID_T) call h5sclose_f(memspace, close_error) + if (filespace >= 0_HID_T) call h5sclose_f(filespace, close_error) + memspace = -1_HID_T + filespace = -1_HID_T + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not configure a fixed-dataset transfer') + end if + end subroutine prepare_full_transfer + + subroutine finish_fixed_write(file, dataset_id, filespace, memspace, path, & + write_error, status) + type(hdf5_file_t), intent(in) :: file + integer(HID_T), intent(inout) :: dataset_id, filespace, memspace + character(len=*), intent(in) :: path + integer, intent(in) :: write_error + type(xdmf_status_t), intent(inout) :: status + + integer :: close_error, delete_error, space_error + + close_error = 0 + delete_error = 0 + space_error = 0 + if (memspace >= 0_HID_T) call h5sclose_f(memspace, space_error) + if (filespace >= 0_HID_T) call h5sclose_f(filespace, close_error) + if (space_error == 0) space_error = close_error + memspace = -1_HID_T + filespace = -1_HID_T + call h5dclose_f(dataset_id, close_error) + dataset_id = -1_HID_T + if (write_error /= 0 .or. close_error /= 0 .or. space_error /= 0) then + call h5ldelete_f(file%id, trim(path), delete_error) + end if + if (write_error /= 0) then + if (delete_error /= 0) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Dataset write and cleanup failed: '//trim(path)) + else + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not write HDF5 dataset: '//trim(path)) + end if + else if (close_error /= 0 .or. space_error /= 0) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Could not close HDF5 dataset resources: '//trim(path)) + end if + end subroutine finish_fixed_write + + subroutine prepare_append(file, path, shape, committed_steps, dataset_id, & + filespace, memspace, new_dims, offset, count, mem_dims, status, & + local_offset, local_count) + type(hdf5_file_t), intent(in) :: file + character(len=*), intent(in) :: path + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps + integer(HID_T), intent(out) :: dataset_id, filespace, memspace + integer(HSIZE_T), allocatable, intent(out) :: new_dims(:), offset(:), count(:) + integer(HSIZE_T), intent(out) :: mem_dims(1) + type(xdmf_status_t), intent(out) :: status + integer(int64), intent(in), optional :: local_offset(:), local_count(:) + + integer(HSIZE_T), allocatable :: dims(:), maxdims(:) + integer(int64) :: memory_elements + integer :: error, close_error, rank + logical :: close_failed, empty_selection + + call set_status_success(status) + dataset_id = -1_HID_T + filespace = -1_HID_T + memspace = -1_HID_T + rank = size(shape) + 1 + allocate(dims(rank), maxdims(rank), new_dims(rank), offset(rank), count(rank)) + if (present(local_offset) .neqv. present(local_count)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'A series hyperslab requires both offset and count') + return + end if + if (present(local_offset)) then + if (size(local_offset) /= size(shape) .or. & + size(local_count) /= size(shape)) then + call set_status_error(status, XDMF_ERROR_ARGUMENT, & + 'A series hyperslab rank must match the dataset rank') + return + end if + end if + + call h5dopen_f(file%id, trim(path), dataset_id, error) + if (error == 0) call h5dget_space_f(dataset_id, filespace, error) + if (error == 0) then + call h5sget_simple_extent_dims_f(filespace, dims, maxdims, error) + if (error >= 0) error = 0 + end if + if (error /= 0) then + call close_append_handles(dataset_id, filespace, memspace, close_failed) + if (close_failed) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Series open and cleanup failed: '//trim(path)) + else + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not open series dataset: '//trim(path)) + end if + return + end if + + if (size(shape) > 0) then + if (any(dims(:rank - 1) /= int(shape, HSIZE_T))) then + call close_append_handles(dataset_id, filespace, memspace, close_failed) + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Series dataset shape changed: '//trim(path)) + return + end if + end if + if (dims(rank) /= int(committed_steps, HSIZE_T)) then + call close_append_handles(dataset_id, filespace, memspace, close_failed) + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Series dataset extent is not synchronized: '//trim(path)) + return + end if + + new_dims = dims + new_dims(rank) = dims(rank) + 1_HSIZE_T + call h5dset_extent_f(dataset_id, new_dims, error) + call h5sclose_f(filespace, close_error) + if (close_error /= 0) then + call close_append_handles(dataset_id, filespace, memspace, close_failed) + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Could not close the original series dataspace: '//trim(path)) + return + end if + filespace = -1_HID_T + if (error == 0) call h5dget_space_f(dataset_id, filespace, error) + + offset = 0_HSIZE_T + offset(rank) = dims(rank) + if (present(local_offset)) then + if (size(shape) > 0) then + offset(:rank - 1) = int(local_offset, HSIZE_T) + count(:rank - 1) = int(local_count, HSIZE_T) + end if + else + count = new_dims + end if + count(rank) = 1_HSIZE_T + empty_selection = .false. + if (present(local_count)) empty_selection = any(local_count == 0_int64) + if (error == 0 .and. empty_selection) then + call h5sselect_none_f(filespace, error) + else if (error == 0) then + call h5sselect_hyperslab_f(filespace, H5S_SELECT_SET_F, & + offset, count, error) + end if + + if (present(local_count)) then + memory_elements = product_int64(local_count) + else + memory_elements = product_int64(shape) + end if + mem_dims(1) = int(max(1_int64, memory_elements), HSIZE_T) + if (error == 0) call h5screate_simple_f(1, mem_dims, memspace, error) + if (error == 0 .and. empty_selection) then + call h5sselect_none_f(memspace, error) + else if (error == 0 .and. .not. present(local_offset) .and. & + file%collective .and. file%rank /= file%root_rank) then + call h5sselect_none_f(filespace, error) + if (error == 0) call h5sselect_none_f(memspace, error) + end if + if (error /= 0) then + call h5dset_extent_f(dataset_id, dims, close_error) + call close_append_handles(dataset_id, filespace, memspace, close_failed) + if (close_error /= 0 .or. close_failed) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Series setup and rollback failed: '//trim(path)) + else + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not extend series dataset: '//trim(path)) + end if + end if + end subroutine prepare_append + + subroutine finish_append(dataset_id, filespace, memspace, path, shape, & + committed_steps, write_error, status) + integer(HID_T), intent(inout) :: dataset_id, filespace, memspace + character(len=*), intent(in) :: path + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: committed_steps, write_error + type(xdmf_status_t), intent(inout) :: status + + integer(HSIZE_T), allocatable :: rollback_dims(:) + integer :: error, rank + logical :: close_failed + + rank = size(shape) + 1 + if (write_error /= 0) then + allocate(rollback_dims(rank)) + if (size(shape) > 0) rollback_dims(:rank - 1) = int(shape, HSIZE_T) + rollback_dims(rank) = int(committed_steps, HSIZE_T) + call h5dset_extent_f(dataset_id, rollback_dims, error) + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Series append and rollback failed: '//trim(path)) + else + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not append series dataset: '//trim(path)) + end if + end if + call close_append_handles(dataset_id, filespace, memspace, close_failed) + if (close_failed) then + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'Could not close series dataset resources: '//trim(path)) + end if + end subroutine finish_append + + subroutine close_append_handles(dataset_id, filespace, memspace, close_failed) + integer(HID_T), intent(inout) :: dataset_id, filespace, memspace + logical, intent(out) :: close_failed + integer :: error + + close_failed = .false. + if (memspace >= 0_HID_T) then + call h5sclose_f(memspace, error) + if (error /= 0) close_failed = .true. + end if + if (filespace >= 0_HID_T) then + call h5sclose_f(filespace, error) + if (error /= 0) close_failed = .true. + end if + if (dataset_id >= 0_HID_T) then + call h5dclose_f(dataset_id, error) + if (error /= 0) close_failed = .true. + end if + memspace = -1_HID_T + filespace = -1_HID_T + dataset_id = -1_HID_T + end subroutine close_append_handles + + subroutine choose_chunk_shape(shape, numeric_type, target_bytes, chunks) + integer(int64), intent(in) :: shape(:) + integer, intent(in) :: numeric_type + integer(int64), intent(in) :: target_bytes + integer(HSIZE_T), intent(out) :: chunks(:) + + integer :: largest(1) + integer(int64) :: chunk_elements, target_elements + + chunks = int(max(shape, 1_int64), HSIZE_T) + target_elements = max(1_int64, target_bytes / & + max(1_int64, numeric_type_size(numeric_type))) + do + chunk_elements = product_int64(int(chunks, int64)) + if (chunk_elements >= 0_int64 .and. & + chunk_elements <= target_elements) exit + largest = maxloc(chunks) + chunks(largest(1)) = max(1_HSIZE_T, (chunks(largest(1)) + 1_HSIZE_T) / 2_HSIZE_T) + end do + end subroutine choose_chunk_shape + + integer(HID_T) function hdf_datatype(numeric_type) + integer, intent(in) :: numeric_type + + select case (numeric_type) + case (XDMF_NUMERIC_REAL32) + hdf_datatype = h5kind_to_type(real32, H5_REAL_KIND) + case (XDMF_NUMERIC_REAL64) + hdf_datatype = h5kind_to_type(real64, H5_REAL_KIND) + case (XDMF_NUMERIC_INT32) + hdf_datatype = h5kind_to_type(int32, H5_INTEGER_KIND) + case (XDMF_NUMERIC_INT64) + hdf_datatype = h5kind_to_type(int64, H5_INTEGER_KIND) + case default; hdf_datatype = -1_HID_T + end select + end function hdf_datatype + + subroutine write_string_attribute(location_id, name, value, status) + integer(HID_T), intent(in) :: location_id + character(len=*), intent(in) :: name, value + type(xdmf_status_t), intent(out) :: status + + integer(HID_T) :: dataspace_id, datatype_id, attribute_id + integer(HSIZE_T) :: dims(1) + integer(SIZE_T) :: string_size + integer :: error, close_error + + call set_status_success(status) + dataspace_id = -1_HID_T + datatype_id = -1_HID_T + attribute_id = -1_HID_T + dims(1) = 1_HSIZE_T + string_size = int(max(1, len_trim(value)), SIZE_T) + call h5screate_f(H5S_SCALAR_F, dataspace_id, error) + if (error == 0) call h5tcopy_f(H5T_FORTRAN_S1, datatype_id, error) + if (error == 0) call h5tset_size_f(datatype_id, string_size, error) + if (error == 0) then + call h5acreate_f(location_id, trim(name), datatype_id, dataspace_id, & + attribute_id, error) + end if + if (error == 0) then + call h5awrite_f(attribute_id, datatype_id, trim(value), dims, error) + end if + if (attribute_id >= 0_HID_T) call h5aclose_f(attribute_id, close_error) + if (datatype_id >= 0_HID_T) call h5tclose_f(datatype_id, close_error) + if (dataspace_id >= 0_HID_T) call h5sclose_f(dataspace_id, close_error) + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_HDF5, & + 'Could not write HDF5 schema attribute: '//trim(name)) + end if + end subroutine write_string_attribute + + subroutine close_after_create_error(file, status) + type(hdf5_file_t), intent(inout) :: file + type(xdmf_status_t), intent(inout) :: status + integer :: error, property_error + + error = 0 + property_error = 0 + if (file%id >= 0_HID_T) call h5fclose_f(file%id, error) + if (file%transfer_property /= H5P_DEFAULT_F) then + call h5pclose_f(file%transfer_property, property_error) + if (property_error == 0) file%transfer_property = H5P_DEFAULT_F + end if + if (error == 0 .and. property_error == 0) then + file%id = -1_HID_T + else + call set_status_error(status, XDMF_ERROR_CONSISTENCY, & + 'HDF5 file creation failed and its handle could not be closed') + end if + end subroutine close_after_create_error + +end module xdmf_hdf5_backend_m diff --git a/external/xdmf-hdf5/src/xdmf_model.F90 b/external/xdmf-hdf5/src/xdmf_model.F90 new file mode 100644 index 000000000..af5671f85 --- /dev/null +++ b/external/xdmf-hdf5/src/xdmf_model.F90 @@ -0,0 +1,424 @@ +module xdmf_model_m + use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64 + + implicit none + + private + + integer, parameter, public :: XDMF_SUCCESS = 0 + integer, parameter, public :: XDMF_ERROR_ARGUMENT = 1 + integer, parameter, public :: XDMF_ERROR_STATE = 2 + integer, parameter, public :: XDMF_ERROR_IO = 3 + integer, parameter, public :: XDMF_ERROR_HDF5 = 4 + integer, parameter, public :: XDMF_ERROR_CONSISTENCY = 5 + + integer, parameter, public :: XDMF_SERIES_NONE = 0 + integer, parameter, public :: XDMF_SERIES_TIME = 1 + integer, parameter, public :: XDMF_SERIES_FREQUENCY = 2 + integer, parameter, public :: XDMF_SERIES_PARAMETER = 3 + + integer, parameter, public :: XDMF_GEOMETRY_UNIFORM = 1 + integer, parameter, public :: XDMF_GEOMETRY_RECTILINEAR = 2 + integer, parameter, public :: XDMF_GEOMETRY_CURVILINEAR = 3 + integer, parameter, public :: XDMF_GEOMETRY_UNSTRUCTURED = 4 + + integer, parameter, public :: XDMF_TOPOLOGY_POLYVERTEX = 1 + integer, parameter, public :: XDMF_TOPOLOGY_POLYLINE = 2 + integer, parameter, public :: XDMF_TOPOLOGY_POLYGON = 3 + integer, parameter, public :: XDMF_TOPOLOGY_TRIANGLE = 4 + integer, parameter, public :: XDMF_TOPOLOGY_QUADRILATERAL = 5 + integer, parameter, public :: XDMF_TOPOLOGY_TETRAHEDRON = 6 + integer, parameter, public :: XDMF_TOPOLOGY_PYRAMID = 7 + integer, parameter, public :: XDMF_TOPOLOGY_WEDGE = 8 + integer, parameter, public :: XDMF_TOPOLOGY_HEXAHEDRON = 9 + integer, parameter, public :: XDMF_TOPOLOGY_POLYHEDRON = 16 + integer, parameter, public :: XDMF_TOPOLOGY_EDGE_3 = 34 + integer, parameter, public :: XDMF_TOPOLOGY_QUADRILATERAL_9 = 35 + integer, parameter, public :: XDMF_TOPOLOGY_TRIANGLE_6 = 36 + integer, parameter, public :: XDMF_TOPOLOGY_QUADRILATERAL_8 = 37 + integer, parameter, public :: XDMF_TOPOLOGY_TETRAHEDRON_10 = 38 + integer, parameter, public :: XDMF_TOPOLOGY_PYRAMID_13 = 39 + integer, parameter, public :: XDMF_TOPOLOGY_WEDGE_15 = 40 + integer, parameter, public :: XDMF_TOPOLOGY_WEDGE_18 = 41 + integer, parameter, public :: XDMF_TOPOLOGY_HEXAHEDRON_20 = 48 + integer, parameter, public :: XDMF_TOPOLOGY_HEXAHEDRON_24 = 49 + integer, parameter, public :: XDMF_TOPOLOGY_HEXAHEDRON_27 = 50 + integer, parameter, public :: XDMF_TOPOLOGY_MIXED = 100 + integer, parameter, public :: XDMF_TOPOLOGY_2D_SMESH = 201 + integer, parameter, public :: XDMF_TOPOLOGY_2D_RECTMESH = 202 + integer, parameter, public :: XDMF_TOPOLOGY_2D_CORECTMESH = 203 + integer, parameter, public :: XDMF_TOPOLOGY_3D_SMESH = 204 + integer, parameter, public :: XDMF_TOPOLOGY_3D_RECTMESH = 205 + integer, parameter, public :: XDMF_TOPOLOGY_3D_CORECTMESH = 206 + + integer, parameter, public :: XDMF_CENTER_NODE = 1 + integer, parameter, public :: XDMF_CENTER_EDGE = 2 + integer, parameter, public :: XDMF_CENTER_FACE = 3 + integer, parameter, public :: XDMF_CENTER_CELL = 4 + integer, parameter, public :: XDMF_CENTER_GRID = 5 + + integer, parameter, public :: XDMF_ATTRIBUTE_SCALAR = 1 + integer, parameter, public :: XDMF_ATTRIBUTE_VECTOR = 2 + integer, parameter, public :: XDMF_ATTRIBUTE_TENSOR = 3 + integer, parameter, public :: XDMF_ATTRIBUTE_TENSOR6 = 4 + integer, parameter, public :: XDMF_ATTRIBUTE_MATRIX = 5 + integer, parameter, public :: XDMF_ATTRIBUTE_GLOBAL_ID = 6 + + integer, parameter, public :: XDMF_NUMERIC_REAL32 = 1 + integer, parameter, public :: XDMF_NUMERIC_REAL64 = 2 + integer, parameter, public :: XDMF_NUMERIC_INT32 = 3 + integer, parameter, public :: XDMF_NUMERIC_INT64 = 4 + + type, public :: xdmf_status_t + private + integer :: code = XDMF_SUCCESS + character(len=:), allocatable :: detail + contains + procedure, public :: is_error => status_is_error + procedure, public :: error_code => status_error_code + procedure, public :: message => status_message + end type xdmf_status_t + + type, public :: xdmf_options_t + logical :: overwrite = .false. + integer :: series_kind = XDMF_SERIES_NONE + integer :: compression_level = 0 + integer(int64) :: chunk_target_bytes = 1048576_int64 + logical :: collective_io = .false. + integer :: communicator = 0 + integer :: root_rank = 0 + end type xdmf_options_t + + type, public :: xdmf_collection_id_t + private + integer :: value = 0 + integer(int64) :: owner = 0_int64 + end type xdmf_collection_id_t + + type, public :: xdmf_grid_id_t + private + integer :: value = 0 + integer(int64) :: owner = 0_int64 + end type xdmf_grid_id_t + + type, public :: xdmf_attribute_id_t + private + integer :: value = 0 + integer(int64) :: owner = 0_int64 + end type xdmf_attribute_id_t + + type, public :: collection_record_t + integer :: id = 0 + character(len=:), allocatable :: name + end type collection_record_t + + type, public :: grid_record_t + integer :: id = 0 + integer :: collection_id = 0 + integer :: geometry_type = 0 + integer :: topology_type = 0 + integer :: dimension = 0 + integer :: geometry_numeric_type = XDMF_NUMERIC_REAL64 + integer :: topology_numeric_type = XDMF_NUMERIC_INT64 + integer :: nodes_per_element = 0 + integer(int64) :: number_of_points = 0_int64 + integer(int64) :: number_of_elements = 0_int64 + integer(int64) :: mixed_connectivity_size = 0_int64 + integer(int64), allocatable :: dimensions(:) + integer(int64), allocatable :: axis_sizes(:) + character(len=:), allocatable :: name + character(len=:), allocatable :: group_path + character(len=:), allocatable :: origin_path + character(len=:), allocatable :: spacing_path + character(len=:), allocatable :: points_path + character(len=:), allocatable :: connectivity_path + character(len=:), allocatable :: axis_paths(:) + end type grid_record_t + + type, public :: attribute_record_t + integer :: id = 0 + integer :: grid_id = 0 + integer :: center = 0 + integer :: attribute_type = 0 + integer :: numeric_type = 0 + integer :: last_step = 0 + logical :: is_series = .false. + logical :: is_written = .false. + integer(int64) :: entity_count = 0_int64 + integer(int64), allocatable :: component_shape(:) + integer(int64), allocatable :: storage_shape(:) + character(len=:), allocatable :: name + character(len=:), allocatable :: dataset_path + end type attribute_record_t + + public :: set_status_success + public :: set_status_error + public :: make_collection_id + public :: make_grid_id + public :: make_attribute_id + public :: collection_id_value + public :: grid_id_value + public :: attribute_id_value + public :: collection_id_owner + public :: grid_id_owner + public :: attribute_id_owner + public :: topology_name + public :: topology_nodes_per_element + public :: topology_is_supported + public :: center_name + public :: attribute_type_name + public :: numeric_type_name + public :: numeric_type_precision + public :: numeric_type_size + public :: product_int64 + +contains + + logical function status_is_error(this) + class(xdmf_status_t), intent(in) :: this + + status_is_error = this%code /= XDMF_SUCCESS + end function status_is_error + + integer function status_error_code(this) + class(xdmf_status_t), intent(in) :: this + + status_error_code = this%code + end function status_error_code + + function status_message(this) result(message) + class(xdmf_status_t), intent(in) :: this + character(len=:), allocatable :: message + + if (allocated(this%detail)) then + message = this%detail + else + message = '' + end if + end function status_message + + subroutine set_status_success(status) + type(xdmf_status_t), intent(out) :: status + + status%code = XDMF_SUCCESS + status%detail = '' + end subroutine set_status_success + + subroutine set_status_error(status, code, message) + type(xdmf_status_t), intent(out) :: status + integer, intent(in) :: code + character(len=*), intent(in) :: message + + status%code = code + status%detail = trim(message) + end subroutine set_status_error + + function make_collection_id(value, owner) result(id) + integer, intent(in) :: value + integer(int64), intent(in), optional :: owner + type(xdmf_collection_id_t) :: id + + id%value = value + if (present(owner)) id%owner = owner + end function make_collection_id + + function make_grid_id(value, owner) result(id) + integer, intent(in) :: value + integer(int64), intent(in), optional :: owner + type(xdmf_grid_id_t) :: id + + id%value = value + if (present(owner)) id%owner = owner + end function make_grid_id + + function make_attribute_id(value, owner) result(id) + integer, intent(in) :: value + integer(int64), intent(in), optional :: owner + type(xdmf_attribute_id_t) :: id + + id%value = value + if (present(owner)) id%owner = owner + end function make_attribute_id + + integer function collection_id_value(id) + type(xdmf_collection_id_t), intent(in) :: id + + collection_id_value = id%value + end function collection_id_value + + integer function grid_id_value(id) + type(xdmf_grid_id_t), intent(in) :: id + + grid_id_value = id%value + end function grid_id_value + + integer function attribute_id_value(id) + type(xdmf_attribute_id_t), intent(in) :: id + + attribute_id_value = id%value + end function attribute_id_value + + integer(int64) function collection_id_owner(id) + type(xdmf_collection_id_t), intent(in) :: id + + collection_id_owner = id%owner + end function collection_id_owner + + integer(int64) function grid_id_owner(id) + type(xdmf_grid_id_t), intent(in) :: id + + grid_id_owner = id%owner + end function grid_id_owner + + integer(int64) function attribute_id_owner(id) + type(xdmf_attribute_id_t), intent(in) :: id + + attribute_id_owner = id%owner + end function attribute_id_owner + + logical function topology_is_supported(topology) + integer, intent(in) :: topology + + topology_is_supported = len(topology_name(topology)) > 0 + end function topology_is_supported + + function topology_name(topology) result(name) + integer, intent(in) :: topology + character(len=:), allocatable :: name + + select case (topology) + case (XDMF_TOPOLOGY_POLYVERTEX); name = 'Polyvertex' + case (XDMF_TOPOLOGY_POLYLINE); name = 'Polyline' + case (XDMF_TOPOLOGY_POLYGON); name = 'Polygon' + case (XDMF_TOPOLOGY_TRIANGLE); name = 'Triangle' + case (XDMF_TOPOLOGY_QUADRILATERAL); name = 'Quadrilateral' + case (XDMF_TOPOLOGY_TETRAHEDRON); name = 'Tetrahedron' + case (XDMF_TOPOLOGY_PYRAMID); name = 'Pyramid' + case (XDMF_TOPOLOGY_WEDGE); name = 'Wedge' + case (XDMF_TOPOLOGY_HEXAHEDRON); name = 'Hexahedron' + case (XDMF_TOPOLOGY_POLYHEDRON); name = 'Polyhedron' + case (XDMF_TOPOLOGY_EDGE_3); name = 'Edge_3' + case (XDMF_TOPOLOGY_QUADRILATERAL_9); name = 'Quadrilateral_9' + case (XDMF_TOPOLOGY_TRIANGLE_6); name = 'Triangle_6' + case (XDMF_TOPOLOGY_QUADRILATERAL_8); name = 'Quadrilateral_8' + case (XDMF_TOPOLOGY_TETRAHEDRON_10); name = 'Tetrahedron_10' + case (XDMF_TOPOLOGY_PYRAMID_13); name = 'Pyramid_13' + case (XDMF_TOPOLOGY_WEDGE_15); name = 'Wedge_15' + case (XDMF_TOPOLOGY_WEDGE_18); name = 'Wedge_18' + case (XDMF_TOPOLOGY_HEXAHEDRON_20); name = 'Hexahedron_20' + case (XDMF_TOPOLOGY_HEXAHEDRON_24); name = 'Hexahedron_24' + case (XDMF_TOPOLOGY_HEXAHEDRON_27); name = 'Hexahedron_27' + case (XDMF_TOPOLOGY_MIXED); name = 'Mixed' + case (XDMF_TOPOLOGY_2D_SMESH); name = '2DSMesh' + case (XDMF_TOPOLOGY_2D_RECTMESH); name = '2DRectMesh' + case (XDMF_TOPOLOGY_2D_CORECTMESH); name = '2DCoRectMesh' + case (XDMF_TOPOLOGY_3D_SMESH); name = '3DSMesh' + case (XDMF_TOPOLOGY_3D_RECTMESH); name = '3DRectMesh' + case (XDMF_TOPOLOGY_3D_CORECTMESH); name = '3DCoRectMesh' + case default; name = '' + end select + end function topology_name + + integer function topology_nodes_per_element(topology) + integer, intent(in) :: topology + + select case (topology) + case (XDMF_TOPOLOGY_POLYVERTEX); topology_nodes_per_element = 1 + case (XDMF_TOPOLOGY_TRIANGLE); topology_nodes_per_element = 3 + case (XDMF_TOPOLOGY_QUADRILATERAL); topology_nodes_per_element = 4 + case (XDMF_TOPOLOGY_TETRAHEDRON); topology_nodes_per_element = 4 + case (XDMF_TOPOLOGY_PYRAMID); topology_nodes_per_element = 5 + case (XDMF_TOPOLOGY_WEDGE); topology_nodes_per_element = 6 + case (XDMF_TOPOLOGY_HEXAHEDRON); topology_nodes_per_element = 8 + case (XDMF_TOPOLOGY_EDGE_3); topology_nodes_per_element = 3 + case (XDMF_TOPOLOGY_QUADRILATERAL_9); topology_nodes_per_element = 9 + case (XDMF_TOPOLOGY_TRIANGLE_6); topology_nodes_per_element = 6 + case (XDMF_TOPOLOGY_QUADRILATERAL_8); topology_nodes_per_element = 8 + case (XDMF_TOPOLOGY_TETRAHEDRON_10); topology_nodes_per_element = 10 + case (XDMF_TOPOLOGY_PYRAMID_13); topology_nodes_per_element = 13 + case (XDMF_TOPOLOGY_WEDGE_15); topology_nodes_per_element = 15 + case (XDMF_TOPOLOGY_WEDGE_18); topology_nodes_per_element = 18 + case (XDMF_TOPOLOGY_HEXAHEDRON_20); topology_nodes_per_element = 20 + case (XDMF_TOPOLOGY_HEXAHEDRON_24); topology_nodes_per_element = 24 + case (XDMF_TOPOLOGY_HEXAHEDRON_27); topology_nodes_per_element = 27 + case default; topology_nodes_per_element = 0 + end select + end function topology_nodes_per_element + + function center_name(center) result(name) + integer, intent(in) :: center + character(len=:), allocatable :: name + + select case (center) + case (XDMF_CENTER_NODE); name = 'Node' + case (XDMF_CENTER_EDGE); name = 'Edge' + case (XDMF_CENTER_FACE); name = 'Face' + case (XDMF_CENTER_CELL); name = 'Cell' + case (XDMF_CENTER_GRID); name = 'Grid' + case default; name = '' + end select + end function center_name + + function attribute_type_name(attribute_type) result(name) + integer, intent(in) :: attribute_type + character(len=:), allocatable :: name + + select case (attribute_type) + case (XDMF_ATTRIBUTE_SCALAR); name = 'Scalar' + case (XDMF_ATTRIBUTE_VECTOR); name = 'Vector' + case (XDMF_ATTRIBUTE_TENSOR); name = 'Tensor' + case (XDMF_ATTRIBUTE_TENSOR6); name = 'Tensor6' + case (XDMF_ATTRIBUTE_MATRIX); name = 'Matrix' + case (XDMF_ATTRIBUTE_GLOBAL_ID); name = 'GlobalID' + case default; name = '' + end select + end function attribute_type_name + + function numeric_type_name(numeric_type) result(name) + integer, intent(in) :: numeric_type + character(len=:), allocatable :: name + + select case (numeric_type) + case (XDMF_NUMERIC_REAL32, XDMF_NUMERIC_REAL64); name = 'Float' + case (XDMF_NUMERIC_INT32, XDMF_NUMERIC_INT64); name = 'Int' + case default; name = '' + end select + end function numeric_type_name + + integer function numeric_type_precision(numeric_type) + integer, intent(in) :: numeric_type + + select case (numeric_type) + case (XDMF_NUMERIC_REAL32, XDMF_NUMERIC_INT32); numeric_type_precision = 4 + case (XDMF_NUMERIC_REAL64, XDMF_NUMERIC_INT64); numeric_type_precision = 8 + case default; numeric_type_precision = 0 + end select + end function numeric_type_precision + + integer(int64) function numeric_type_size(numeric_type) + integer, intent(in) :: numeric_type + + numeric_type_size = int(numeric_type_precision(numeric_type), int64) + end function numeric_type_size + + integer(int64) function product_int64(values) + integer(int64), intent(in) :: values(:) + integer :: i + + product_int64 = 1_int64 + do i = 1, size(values) + if (values(i) < 0_int64) then + product_int64 = -1_int64 + return + end if + if (values(i) > 0_int64) then + if (product_int64 > huge(product_int64) / values(i)) then + product_int64 = -1_int64 + return + end if + end if + product_int64 = product_int64 * values(i) + end do + end function product_int64 + +end module xdmf_model_m diff --git a/external/xdmf-hdf5/src/xdmf_xml.F90 b/external/xdmf-hdf5/src/xdmf_xml.F90 new file mode 100644 index 000000000..185dc1b84 --- /dev/null +++ b/external/xdmf-hdf5/src/xdmf_xml.F90 @@ -0,0 +1,460 @@ +module xdmf_xml_m + use, intrinsic :: iso_fortran_env, only: int64, real64 + use xdmf_model_m, only: xdmf_status_t, collection_record_t, & + grid_record_t, attribute_record_t, XDMF_ERROR_IO, & + XDMF_SERIES_NONE, XDMF_SERIES_TIME, XDMF_SERIES_FREQUENCY, & + XDMF_GEOMETRY_UNIFORM, XDMF_GEOMETRY_RECTILINEAR, & + XDMF_GEOMETRY_CURVILINEAR, XDMF_GEOMETRY_UNSTRUCTURED, & + XDMF_TOPOLOGY_MIXED, XDMF_TOPOLOGY_2D_SMESH, & + XDMF_TOPOLOGY_2D_RECTMESH, XDMF_TOPOLOGY_2D_CORECTMESH, & + XDMF_TOPOLOGY_3D_SMESH, XDMF_TOPOLOGY_3D_RECTMESH, & + XDMF_TOPOLOGY_3D_CORECTMESH, topology_name, center_name, & + attribute_type_name, numeric_type_name, numeric_type_precision, & + set_status_success, set_status_error + + implicit none + + private + + public :: write_xdmf_document + +contains + + subroutine write_xdmf_document(path, hdf5_name, collections, grids, & + attributes, series_kind, series_values, status) + character(len=*), intent(in) :: path, hdf5_name + type(collection_record_t), intent(in) :: collections(:) + type(grid_record_t), intent(in) :: grids(:) + type(attribute_record_t), intent(in) :: attributes(:) + integer, intent(in) :: series_kind + real(real64), intent(in) :: series_values(:) + type(xdmf_status_t), intent(out) :: status + + integer :: unit, error, step + + call set_status_success(status) + open(newunit=unit, file=trim(path), status='replace', action='write', & + iostat=error) + if (error /= 0) then + call set_status_error(status, XDMF_ERROR_IO, & + 'Could not create XDMF file: '//trim(path)) + return + end if + + call put_line(unit, 0, '', status) + call put_line(unit, 0, '', status) + call put_line(unit, 1, '', status) + call put_line(unit, 2, & + '', & + status) + + if (series_kind == XDMF_SERIES_NONE) then + call write_spatial_contents(unit, 2, hdf5_name, collections, grids, & + attributes, 0, 0, status) + else + if (series_kind == XDMF_SERIES_TIME) then + call put_line(unit, 2, & + '', status) + else + call put_line(unit, 2, & + '', status) + end if + + do step = 1, size(series_values) + call write_series_step_header(unit, 3, series_kind, step, & + series_values(step), status) + call write_spatial_contents(unit, 4, hdf5_name, collections, grids, & + attributes, step, size(series_values), status) + call put_line(unit, 3, '', status) + end do + call put_line(unit, 2, '', status) + end if + + call put_line(unit, 1, '', status) + call put_line(unit, 0, '', status) + close(unit, iostat=error) + if (error /= 0 .and. .not. status%is_error()) then + call set_status_error(status, XDMF_ERROR_IO, & + 'Could not close XDMF file: '//trim(path)) + end if + end subroutine write_xdmf_document + + subroutine write_series_step_header(unit, indent, series_kind, step, & + value, status) + integer, intent(in) :: unit, indent, series_kind, step + real(real64), intent(in) :: value + type(xdmf_status_t), intent(inout) :: status + + character(len=:), allocatable :: step_name, value_text + + step_name = 'Step '//integer_string(int(step, int64)) + value_text = real_string(value) + call put_line(unit, indent, '', status) + if (series_kind == XDMF_SERIES_TIME) then + call put_line(unit, indent + 1, '