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..79254ef0c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,46 +1,69 @@ -// 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", + "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/.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..5924ac633 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,10 @@ .git build/ +build-*/ .venv/ +.devcontainer/ +.vscode/ +.github/ __pycache__/ *.pyc *.pyo @@ -9,4 +13,24 @@ __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/ + +# Linux Docker builds only use the GNU LAPACK bundle. +precompiled_libraries/** +!precompiled_libraries/ +!precompiled_libraries/linux-gcc/ +!precompiled_libraries/linux-gcc/** + +# 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/.fortlsrc b/.fortlsrc new file mode 100644 index 000000000..9f5019a46 --- /dev/null +++ b/.fortlsrc @@ -0,0 +1,18 @@ +{ + "source_dirs": [ + "./src_conformal", + "./src_json_parser", + "./src_main_pub", + "./src_mtln", + "./src_output", + "./src_pyWrapper", + "./src_utils", + "./src_wires_pub", + "./test" + ], + "excl_paths": [ + "./build*", + "./.venv", + "./Testing" + ] +} diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index c86b9f4d4..c5a57637a 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -70,7 +70,7 @@ jobs: if: matrix.compiler.name=='gcc' run: | sudo apt update - sudo apt install libhdf5-dev libopenmpi-dev + sudo apt install libhdf5-dev libhdf5-openmpi-dev libopenmpi-dev - name: Setup fortran compiler uses: fortran-lang/setup-fortran@v1 @@ -92,12 +92,59 @@ jobs: - name: Run unit tests run: build/bin/fdtd_tests + - name: Verify output (serial) + if: matrix.mpi=='OFF' + run: build/bin/fdtd_tests --gtest_filter='output.*' + + - name: Verify output (MPI collective) + if: matrix.mpi=='ON' + run: ctest --test-dir build --output-on-failure -R '^output_mpi_collective_and_root_aggregation$' + + - name: Verify output (MPI root aggregation) + if: matrix.mpi=='ON' + run: ctest --test-dir build --output-on-failure -R '^output_mpi_collective_and_root_aggregation$' + + - name: Run CTest suites + run: ctest --test-dir build --output-on-failure + - name: Run python 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/ --durations=20 + + 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 libhdf5-openmpi-dev libopenmpi-dev python3-h5py python3-numpy + + - name: Configure parallel HDF5 output + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DSEMBA_FDTD_ENABLE_MPI=ON \ + -DSEMBA_FDTD_ENABLE_HDF=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 + 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 dee927d42..93adec4d2 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -71,6 +71,12 @@ jobs: - name: Run unit tests run: build/bin/fdtd_tests.exe + - name: Verify output (serial) + run: build/bin/fdtd_tests.exe --gtest_filter=output.* + + - name: Run CTest suites + run: ctest --test-dir build --output-on-failure + - name: Run python tests (except codemodel) env: SEMBA_FDTD_ENABLE_MPI: ${{ matrix.mpi }} 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..b6f3de0ef --- /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`, `CompileWithNewOutputModule`, `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..476013a44 --- /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`, `CompileWithNewOutputModule`, 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..21455a9e1 --- /dev/null +++ b/.vscode/launch.dev.json @@ -0,0 +1,278 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug semba-fdtd (dbg)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/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}/build/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}/build/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}/build/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}/build/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}/build/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}/build/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}/build/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}/build/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}/build/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}/build/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}/build/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}/build/bin/semba-fdtd", + "SEMBA_FDTD_ENABLE_MPI": "OFF", + "SEMBA_FDTD_ENABLE_MTLN": "ON", + "SEMBA_FDTD_ENABLE_HDF": "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}/build/bin/semba-fdtd", + "SEMBA_FDTD_ENABLE_MPI": "ON", + "SEMBA_FDTD_ENABLE_MTLN": "ON", + "SEMBA_FDTD_ENABLE_HDF": "ON" + } + }, + { + "name": "Attach to process", + "type": "cppdbg", + "request": "attach", + "program": "${workspaceFolder}/build/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..e5b273240 --- /dev/null +++ b/.vscode/settings.dev.json @@ -0,0 +1,25 @@ +{ + "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..5a742c13e --- /dev/null +++ b/.vscode/tasks.dev.json @@ -0,0 +1,91 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "CMake: configure Release no-MPI", + "type": "shell", + "command": "cmake --preset rls", + "problemMatcher": [] + }, + { + "label": "CMake: build Release no-MPI", + "type": "shell", + "command": "cmake --build --preset rls", + "problemMatcher": [] + }, + { + "label": "CMake: configure Debug no-MPI", + "type": "shell", + "command": "cmake --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 GNU strict no-MTLN", + "type": "shell", + "command": "cmake --preset dbg-gnu-strict", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug GNU strict no-MTLN", + "type": "shell", + "command": "cmake --build --preset dbg-gnu-strict", + "dependsOn": ["CMake: configure Debug GNU strict 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 --preset rls-mpi", + "problemMatcher": [] + }, + { + "label": "CMake: build Release MPI", + "type": "shell", + "command": "cmake --build --preset rls-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_FDTD_ENABLE_HDF=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_FDTD_ENABLE_HDF=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..14181551f --- /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}/build/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/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_FDTD_ENABLE_HDF=ON SEMBA_EXE=build/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_FDTD_ENABLE_HDF=ON SEMBA_EXE=build/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/CLAUDE.md index 254fa4d5f..136d88fe7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,13 +10,12 @@ 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 --fresh --preset rls cmake --build build -j ``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 06c3cadbc..a0134845e 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required (VERSION 3.15) -project(semba-fdtd Fortran) +project(semba-fdtd Fortran C) enable_language (Fortran) @@ -12,13 +12,23 @@ set(CMAKE_Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/mod) 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_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_DOUBLE_PRECISION "Use double precision (CompileWithReal8)" OFF) +option(SEMBA_FDTD_ENABLE_GNU_STRICT_WARNINGS "Enable additional GNU Fortran warnings" OFF) + +option(SEMBA_FDTD_ENABLE_TEST "Compile tests" ON) + +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) @@ -27,13 +37,30 @@ 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) -# Compilation defines. + +if(SEMBA_FDTD_ENABLE_MPI AND SEMBA_FDTD_ENABLE_HDF) + set(HDF5_PREFER_PARALLEL TRUE) +endif() + +if(SEMBA_FDTD_ENABLE_HDF) + set(XDMF_HDF5_ENABLE_MPI "${SEMBA_FDTD_ENABLE_MPI}") +endif() + +# Compilation defines. if(CMAKE_BUILD_TYPE MATCHES "Release" OR CMAKE_BUILD_TYPE MATCHES "release" ) add_definitions(-DCompileWithRelease) else() add_definitions(-DCompileWithDebug) endif() -if(SEMBA_FDTD_ENABLE_SMBJSON) +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() if (SEMBA_FDTD_ENABLE_MTLN) @@ -44,12 +71,21 @@ if (SEMBA_FDTD_ENABLE_DOUBLE_PRECISION) else() add_definitions(-DCompileWithReal4) endif() -add_definitions( - -DCompileWithInt2 - -DCompileWithOpenMP -) - -include("${CMAKE_CURRENT_SOURCE_DIR}/set_precompiled_libraries.cmake") +add_definitions( + -DCompileWithInt2 + -DCompileWithOpenMP + -DCompileWithNewOutputModule +) + +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") if (CMAKE_SYSTEM_NAME MATCHES "Linux") message(STATUS "Using Linux flags") @@ -57,6 +93,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") @@ -64,9 +103,13 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux") set(CMAKE_CXX_FLAGS_RELEASE "-Ofast") set(CMAKE_Fortran_FLAGS_RELEASE "-Ofast") - set(CMAKE_C_FLAGS_DEBUG "-g -O0") - set(CMAKE_CXX_FLAGS_DEBUG "-g -O0") - set(CMAKE_Fortran_FLAGS_DEBUG "-g -O0 -fno-inline -fcheck=all -fbacktrace") + set(CMAKE_C_FLAGS_DEBUG "-g -O0") + set(CMAKE_CXX_FLAGS_DEBUG "-g -O0") + set(CMAKE_Fortran_FLAGS_DEBUG "-g -O0 -fno-inline -fcheck=all -fbacktrace") + + if(SEMBA_FDTD_ENABLE_GNU_STRICT_WARNINGS) + set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} -Wall -Wextra -Wimplicit-interface -Wimplicit-procedure -Wuse-without-only") + endif() elseif(CMAKE_Fortran_COMPILER_ID MATCHES "IntelLLVM") message(STATUS "Using IntelLLVM (ifx) flags") @@ -159,7 +202,10 @@ 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) +if(SEMBA_FDTD_ENABLE_HDF) + target_link_libraries(semba-reports XDMF::HDF5) +endif() if(SEMBA_FDTD_ENABLE_SMBJSON) add_subdirectory(src_json_parser) @@ -180,6 +226,12 @@ if (SEMBA_FDTD_ENABLE_MTLN) endif() endif() +add_subdirectory(src_utils) + +add_subdirectory(src_output) +set(OUTPUT_LIBRARIES fdtd-output) +set(VTK_API_LIBRARIES vtkAPI) + add_subdirectory(src_conformal) set(CONFORMAL_LIBRARIES conformal) @@ -188,6 +240,8 @@ if (SEMBA_FDTD_ENABLE_TEST) add_subdirectory(test) endif() + + if(SEMBA_FDTD_COMPONENTS_LIB) add_library(semba-components "src_main_pub/anisotropic.F90" @@ -210,17 +264,15 @@ if(SEMBA_FDTD_COMPONENTS_LIB) 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}) + add_library(semba-outputs + "src_main_pub/mpicomm.F90" + # Retained for the standalone -createh5filefromsinglebin utility. + "src_main_pub/xdmf_h5.F90" + ) + target_link_libraries(semba-outputs + semba-components + XDMF::HDF5 + ${MPI_Fortran_LIBRARIES}) endif() if(SEMBA_FDTD_MAIN_LIB) @@ -243,15 +295,17 @@ if(SEMBA_FDTD_MAIN_LIB) "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/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 - ${SMBJSON_LIBRARIES} + semba-outputs + fdtd-utils + ${OUTPUT_LIBRARIES} + ${VTK_API_LIBRARIES} + ${SMBJSON_LIBRARIES} ${MTLN_LIBRARIES}) endif() diff --git a/CMakePresets.json b/CMakePresets.json index 3f068ff06..b38467d5a 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -4,31 +4,57 @@ { "name": "rls", "generator": "Ninja", - "binaryDir": "build-rls/", + "binaryDir": "build/", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "SEMBA_FDTD_ENABLE_MPI": "OFF" } }, { "name": "dbg", "inherits": "rls", - "binaryDir": "build-dbg/", + "binaryDir": "build/", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } }, + { + "name": "rls-mpi", + "inherits": "rls", + "binaryDir": "build/", + "cacheVariables": { + "SEMBA_FDTD_ENABLE_MPI": "ON" + } + }, + { + "name": "dbg-mpi", + "inherits": "dbg", + "binaryDir": "build/", + "cacheVariables": { + "SEMBA_FDTD_ENABLE_MPI": "ON" + } + }, { "name": "dbg-nomtln", "inherits": "dbg", - "binaryDir": "build-dbg-nomtln/", + "binaryDir": "build/", "cacheVariables": { "SEMBA_FDTD_ENABLE_MTLN": "OFF" } }, + { + "name": "dbg-gnu-strict", + "inherits": "dbg", + "binaryDir": "build-dbg-gnu-strict/", + "cacheVariables": { + "SEMBA_FDTD_ENABLE_MTLN": "OFF", + "SEMBA_FDTD_ENABLE_GNU_STRICT_WARNINGS": "ON" + } + }, { "name": "rls-nomtln", "inherits": "rls", - "binaryDir": "build-rls-nomtln/", + "binaryDir": "build/", "cacheVariables": { "SEMBA_FDTD_ENABLE_MTLN": "OFF" } @@ -36,7 +62,7 @@ { "name": "intel-rls", "generator": "Ninja", - "binaryDir": "build-intel-rls/", + "binaryDir": "build/", "environment": { "I_MPI_ROOT": "/opt/intel/oneapi/mpi/latest", "FC": "mpiifx", @@ -53,10 +79,48 @@ { "name": "intel-rls-nomtln", "inherits": "intel-rls", - "binaryDir": "build-intel-rls-nomtln/", + "binaryDir": "build/", "cacheVariables": { "SEMBA_FDTD_ENABLE_MTLN": "OFF" } } + ], + "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": "dbg-gnu-strict", + "configurePreset": "dbg-gnu-strict" + }, + { + "name": "intel-rls", + "configurePreset": "intel-rls" + }, + { + "name": "intel-rls-nomtln", + "configurePreset": "intel-rls-nomtln" + } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38f8a05c7..1939e2547 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,6 +70,21 @@ pytest test/ (You can also use `python -m pytest test/`.) +### Fortran tooling + +The development container provides Modern Fortran, `fortls`, `fprettify`, and +Fortitude. Reload VS Code after rebuilding the container to activate the +extensions and tools. + +Run `fortitude check --exit-zero` to review Fortitude's advisory diagnostics. +It is not a required gate because the current preprocessed sources produce +parser false positives. + +Use `cmake --preset dbg-gnu-strict` and +`cmake --build --preset dbg-gnu-strict` to compile the no-MTLN configuration +with additional GNU Fortran warnings. The preset is opt-in and does not alter +the established build presets. + ## Making changes - Keep changes focused and as small as reasonably possible. @@ -134,4 +149,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..37ab94b6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,58 +1,157 @@ -# ─── 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 fortitude-lint -# 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 \ +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 \ + cmake \ + make \ + ninja-build \ + && locale-gen en_US.UTF-8 \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /src/build/bin/semba-fdtd /usr/local/bin/semba-fdtd +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; \ + chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}/workspaces + +COPY docker/intel-quality-entrypoint.sh /usr/local/bin/intel-quality-entrypoint +RUN chmod 0755 /usr/local/bin/intel-quality-entrypoint -WORKDIR /work +USER ${USERNAME} +WORKDIR /home/${USERNAME}/workspaces/fdtd -ENTRYPOINT ["semba-fdtd", "-i"] +ENTRYPOINT ["/usr/local/bin/intel-quality-entrypoint"] +CMD ["bash", "-l"] diff --git a/doc/binary-probe-output-implementation.md b/doc/binary-probe-output-implementation.md new file mode 100644 index 000000000..35f9fe596 --- /dev/null +++ b/doc/binary-probe-output-implementation.md @@ -0,0 +1,181 @@ +# Guía de Implementación de Salida Binaria de Sondas + +## Propósito + +Esta guía documenta los cambios que añaden resultados binarios a las sondas +muestreadas, sin eliminar sus resultados de texto o visualización. +Incluye sondas de punto, hilo, bloque, línea, campo lejano y volumétricas. +La salida exclusivamente geométrica no cambia. + +## Flujo de Salida + +```mermaid +flowchart TD + A[Observación configurada] --> B[Inicializar sonda] + B --> C[Declarar texto y binario] + C --> D[Crear binario vacío] + D --> E[Muestrear campos] + E --> F[Acumular en búfer] + F --> G[Volcar texto] + G --> H[Escribir registros binarios] + H --> I[Finalizar metadatos] + I --> J[Manifiesto de salida] + + K[Solver de campo lejano] --> L[Fila final calculada] + L --> M[Texto heredado] + L --> N[Registro binario] +``` + +`output_m` coordina el ciclo de vida. +Cada módulo de sonda conserva la responsabilidad de recoger sus muestras y +publicar sus datos. +`outputTypes_m` define el contrato del artefacto y `outputBinary_m` codifica +valores little-endian. + +El campo lejano es una excepción deliberada. +Sus valores finales solo existen dentro de `FlushFarfield`, por lo que el solver +escribe su registro binario directamente y no reconstruye datos desde texto. + +## Ciclo de Vida + +```mermaid +stateDiagram-v2 + [*] --> Declarado: inicialización + Declarado --> Activo: comienza el muestreo + Activo --> Activo: muestra o volcado + Activo --> Finalizando: termina simulación + Finalizando --> Completo: artefactos requeridos disponibles + Finalizando --> Fallido: error de publicación + Declarado --> Completo: salida sin muestras +``` + +Los artefactos binarios escalares se crean durante la inicialización. +Por tanto, un archivo binario vacío representa una sonda válida sin muestras y +no una salida ausente. + +## Contrato Binario + +Cada `output_artifact_t` binario declara: + +- Ruta relativa y tipo de artefacto. +- Rol canónico o de fragmento. +- Orden de bytes. +- Representación numérica y de complejos. +- Tamaño del registro. +- Orden de componentes. + +Los registros nuevos usan IEEE `float64` little-endian. +La compilación por defecto mezcla valores de campo de precisión simple con +tiempo y valores complejos de precisión doble. +Usar un único ancho `float64` evita perder precisión al combinar esos valores +en un registro. + +```mermaid +flowchart LR + A[Declaración] --> B{¿Formato válido?} + B -- no --> C[Error de escritor] + B -- sí --> D[Abrir o crear flujo] + D --> E[Codificar bytes little-endian] + E --> F[Anexar o reemplazar] + F --> G[Estado de publicación] +``` + +## Formatos de Registro + +Todas las entradas son `float64` little-endian. + +| Sonda o serie | Orden de componentes | Bytes | +|---|---|---:| +| Punto temporal | `time,value` | 16 | +| Punto en frecuencia | `frequency,value.real,value.imag` | 24 | +| Carga de hilo | `time,charge` | 16 | +| Corriente de hilo | `time,current,delta_voltage,plus_voltage,minus_voltage,voltage_difference` | 48 | +| Bloque | `time,value` | 16 | +| Línea | `time,line_integral` | 16 | +| Volumétrica temporal | `time,x,y,z,Ex,Ey,Ez` | 56 | +| Volumétrica en frecuencia | `frequency,x,y,z,Ex.real,Ex.imag,Ey.real,Ey.imag,Ez.real,Ez.imag` | 80 | +| Campo lejano | `frequency,theta,phi,Etheta.magnitude,Etheta.phase,Ephi.magnitude,Ephi.phase,RCS.arithmetic,RCS.geometric` | 72 | + +Las sondas volumétricas de componente específico conservan los componentes no +usados como cero dentro de su registro vectorial de tamaño fijo. +El campo lejano usa magnitud y fase, igual que su salida textual existente. + +## Cambios por Familia + +### Punto + +`point_probe_output_t` reserva cuatro artefactos cuando se solicitan ambos +dominios: texto y binario para tiempo, y texto y binario para frecuencia. +Los registros temporales se anexan en cada volcado. +Los registros de frecuencia reemplazan el resultado integrado completo. + +### Hilo, Bloque y Línea + +Las sondas de corriente y carga de hilo, de bloque y de línea conservan texto y +anexan registros binarios después de una escritura de texto correcta. +Los registros de línea cambiaron de `float32` a `float64`. +Los búferes de hilo y bloque solo se limpian cuando la publicación binaria tiene +éxito. + +### Volumétrica + +`movieProbeOutput_m` convirtió sus registros a siete `float64` por muestra. +`frequencySliceProbeOutput_m` convirtió sus registros a diez `float64`, con +pares real/imaginario para cada componente vectorial. +Ambas mantienen sus artefactos de visualización. + +### Campo Lejano + +`farfield_m` crea un archivo `.bin` junto al resultado de texto y escribe una +fila de nueve `float64` para cada resultado angular final. +`farFieldProbeOutput_m` declara el archivo y su formato en los metadatos. + +## Propiedad Distribuida + +```mermaid +flowchart LR + A[Muestra solicitada] --> B{Familia} + B -->|Escalar o línea| C[Elegir escritor canónico] + B -->|Volumétrica| D[Partición local disjunta] + C --> E[Un publicador] + D --> F[Un propietario por ubicación] + E --> G[Artefacto canónico] + F --> G +``` + +La funcionalidad binaria reutiliza la selección de propietario para puntos, las +reglas de interfaz de línea y las particiones volumétricas existentes. +Así evita escrituras concurrentes y duplicados en las fronteras. + +## Archivos Modificados + +| Área | Archivos principales | +|---|---| +| Contrato | `src_output/outputTypes.F90` | +| Escritor portable | `src_output/outputBinary.F90` | +| Sondas escalares | `src_output/pointProbeOutput.F90`, `src_output/wireProbeOutput.F90`, `src_output/bulkProbeOutput.F90`, `src_output/lineProbeOutput.F90` | +| Sondas volumétricas | `src_output/movieProbeOutput.F90`, `src_output/frequencySliceProbeOutput.F90` | +| Campo lejano | `src_main_pub/farfield.F90`, `src_output/farFieldProbeOutput.F90` | +| Pruebas | `test/unit/output/output_tests.h`, `test/unit/output/test_output_binary.F90`, `test/unit/output/test_probe_output.F90` | + +## Validación + +Las pruebas cubren selección de precisión, registros complejos, anexado +`float64`, publicación de punto, hilo, bloque y línea, artefactos sin muestras, +tamaño de fila de campo lejano, registros volumétricos y contratos de partición. + +Ejecutar: + +```bash +build/bin/fdtd_tests --gtest_filter='output.*' +``` + +En esta implementación, las 52 pruebas de salida pasaron. + +## Uso por Consumidores + +- Resolver la ruta binaria desde los metadatos, no desde convenciones de nombre. +- Interpretar las columnas con `component_order`. +- Usar `record_bytes` para leer registros completos. +- Aplicar el orden de bytes y la representación de complejos declarados. +- Mantener los resultados de texto para revisión manual o flujos existentes. diff --git a/doc/development.md b/doc/development.md index a4dd8aa4b..d7372a69a 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,13 @@ 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 +``` + +All local configurations use `build/`. Run CMake with `--fresh` whenever changing compiler, build type, or feature options so the previous cache is discarded. If you use intel oneapi compiler, make sure to run @@ -23,24 +46,62 @@ 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 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 +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 +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 library can be set with the option `-DHDF5_ROOT=`, e.g. +A specific HDF5 installation can be selected with +`-DHDF5_ROOT=`: ```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 \ + -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 +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 +``` + +MPI builds automatically prefer parallel HDF5 when it is available. +Select the parallel HDF5 installation while enabling the normal MPI option: + +```shell +cmake -S . -B build-parallel \ + -DSEMBA_FDTD_ENABLE_MPI=ON \ + -DSEMBA_FDTD_ENABLE_HDF=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 +154,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 +179,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 +326,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 @@ -341,55 +406,246 @@ 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 + +| 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. | -gdb is a serial debugger, but can be attached to one of the parallel processes after they have started running. +Copy the version-controlled files template when initially configuring the +workspace, or when the template changes: +```shell +cp .vscode/launch.dev.json .vscode/launch.json +``` -1. Modify the file launch.json to attach to a running process after launching the debugger: +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 ``` -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 +All CMake presets currently share `build/`. +Running a Release or non-MPI configure replaces the previous build +configuration, so rerun the commands above before MPI debugging when needed. + +#### 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} +``` + +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** + +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. -Run the following command as super user: +**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/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..2d48e5196 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -1,245 +1,162 @@ # Docker -This document explains how to use Docker to build, test, and run semba-fdtd without installing any dependencies on your machine. +Docker provides three environments for working with semba-fdtd. +All environments mount the repository at `/home/developer/workspaces/fdtd`, so build artefacts and simulation output are written to the host workspace. +All presets use the same `build/` directory. +Run CMake with `--fresh` whenever switching preset so the previous compiler and configuration 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) -```bash -git submodule update --init --recursive -``` - -## Included files - -| 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 | +## Prerequisite ---- - -## Building the images +Initialise submodules before building an image: ```bash -docker compose build test # image for tests -docker compose build solver # runtime image for simulations +git submodule update --init --recursive ``` -`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`: +## Environments -| Argument | Values | Default | +| Service | Purpose | Included tools | |---|---|---| -| `BUILD_TYPE` | `Release`, `Debug` | `Release` | -| `ENABLE_MPI` | `ON`, `OFF` | `OFF` | -| `ENABLE_MTLN` | `ON`, `OFF` | `ON` | +| `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` | Interactive development and the Dev Container | Everything in `quality`, plus Git, GitHub CLI, SSH client, Node/npm, OpenCode, Fortran tools, and debuggers | -```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 -``` - -**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. +`quality` intentionally excludes developer-only tools. +`quality` and `dev` include ParaView. +`intel-quality` is for Intel-specific compilation checks; it is not the Python test environment or the Dev Container. -### Base image digest +## Build Images -The `Dockerfile` pins the base image using a SHA256 digest instead of just the tag: +Build `quality` first: -```dockerfile -FROM ubuntu:22.04@sha256:eb29ed27... AS builder +```bash +docker compose build 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 `dev` when its additional developer tools are required: ```bash -docker pull ubuntu:22.04 -docker inspect ubuntu:22.04 --format='{{index .RepoDigests 0}}' +docker compose build dev ``` -Then replace both occurrences of the digest in the `Dockerfile` (builder and runtime stages). - ---- - -## Running the tests +Build the Intel oneAPI validation environment when checking Intel-specific configurations: ```bash -docker compose run --rm test +docker compose build intel-quality ``` -This runs in sequence: -1. `build/bin/fdtd_tests` — unit tests (GoogleTest) -2. `python3 -m pytest test/ --durations=20` — Python integration tests +The `dev` target is built from `quality`. +Docker therefore reuses the compiler and ParaView layers when building `dev`. -To run only part of the test suite: +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. -```bash -# Unit tests only -docker compose run --rm test build/bin/fdtd_tests +## Compile With Quality -# 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 -``` - -To open an interactive shell inside the container: +Run CMake presets from the mounted workspace: ```bash -docker compose run --rm --entrypoint bash test +docker compose run --rm quality cmake --fresh --preset rls +docker compose run --rm quality cmake --build --preset rls ``` ---- +For an MPI build: -## Debugging a simulation +```bash +docker compose run --rm quality cmake --fresh --preset rls-mpi +docker compose run --rm quality cmake --build --preset rls-mpi +``` -This uses `gdbserver` inside the container and connects VSCode to it via the C/C++ extension. +The active build remains in `build/` after the container exits. -### Prerequisites +## Compile With Intel oneAPI -- VSCode extension: **C/C++** (`ms-vscode.cpptools`) -- `gdb` installed on the host: - ```bash - sudo apt install gdb - ``` +`intel-quality` initialises Intel oneAPI and Intel MPI automatically. +It also selects the bundled Intel HDF5 runtime, so no manual environment setup is required. -### 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/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/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: +A terminal inside the container should appear. -``` -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. - -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. - -```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 - } - ] - } - ] -} -``` +## Run Binaries And Examples -> The `sourceFileMap` maps `/src` (container path baked into debug info) to `${workspaceFolder}` on the host, so source files display correctly. - ---- - -## Running a simulation - -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/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. - ---- +Any output generated by the solver is available immediately in the corresponding host directory. -## Managing images and containers - -### 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/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. - -### Does closing the terminal stop the container? +## Development Environment -- **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. 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-module-dependencies.mermaid.md b/doc/output-module-dependencies.mermaid.md new file mode 100644 index 000000000..b7ac7cc8c --- /dev/null +++ b/doc/output-module-dependencies.mermaid.md @@ -0,0 +1,281 @@ +# Output Module Dependencies + +This diagram records the direct source-level dependencies of `src_output`. +It also shows the target-level dependencies that CMake uses to assemble the +output library. + +Solid arrows represent direct Fortran `use` relationships or direct CMake +target links. +Dotted arrows are conditional dependencies enabled by compile options. +The diagram uses top-to-bottom dependency layers and gently curved connectors +to reduce long diagonal crossings without making the arrows overly square. + +## Architectural Overview + +Use this view first. +It groups the implementation into dependency boundaries instead of showing +every individual import. + +```mermaid +%%{init: {"flowchart": {"curve": "monotoneY"}}}%% +flowchart TB + COORD["Coordinator\noutput_m"] + CONTRACTS["Contracts and shared state\noutputTypes_m, domain_m"] + PROBES["Probe implementations\npoint, wire, bulk, movie, frequency, far field, VTK"] + SERVICES["Shared output services\noutputUtils, volumicProbeUtils"] + PUBLICATION["Publication adapters\nmetadata, binary, visualisation"] + DISTRIBUTED["Distributed output\ndecomposition, collective, transport"] + + subgraph PROJECT["Project libraries"] + TYPES["semba-types"] + COMPONENTS["semba-components / semba-reports"] + UTILS["fdtd-utils"] + end + + subgraph EXTERNAL["External and optional dependencies"] + XDMF["XDMF::HDF5"] + MPI["MPI"] + WIRES["Wire and MTLN implementations"] + VTK["vtkAPI"] + end + + COORD --> CONTRACTS + COORD --> SERVICES + COORD --> PROBES + COORD --> DISTRIBUTED + PROBES --> CONTRACTS + PROBES --> SERVICES + PROBES --> PUBLICATION + PROBES --> DISTRIBUTED + CONTRACTS --> TYPES + SERVICES --> TYPES + SERVICES --> UTILS + PUBLICATION --> UTILS + PUBLICATION --> XDMF + DISTRIBUTED --> MPI + PROBES --> COMPONENTS + PROBES --> WIRES + PROBES --> VTK + + classDef coordinator fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#111827; + classDef boundary fill:#dcfce7,stroke:#16a34a,color:#111827; + classDef adapter fill:#fef3c7,stroke:#d97706,color:#111827; + classDef project fill:#e5e7eb,stroke:#374151,color:#111827; + classDef external fill:#f3e8ff,stroke:#9333ea,color:#111827; + class COORD coordinator; + class CONTRACTS,PROBES,SERVICES,PUBLICATION,DISTRIBUTED boundary; + class TYPES,COMPONENTS,UTILS project; + class XDMF,MPI,WIRES,VTK external; +``` + +The detailed graph below is the audit view. +It preserves direct source and CMake relationships, but should not be used as +the primary architecture diagram. + +## Direct Module Graph + +```mermaid +%%{init: {"flowchart": {"curve": "monotoneY"}}}%% +flowchart TB + subgraph OUT["src_output / fdtd-output"] + direction TB + subgraph COORD["Coordinator and contracts"] + direction LR + OM["output_m\noutput.F90"] + OT["outputTypes_m\noutputTypes.F90"] + end + subgraph SERVICES["Shared services"] + direction LR + OU["outputUtils_m\noutputUtils.F90"] + DOM["domain_m\ndomain.F90"] + VP["volumicProbeUtils_m\nvolumicProbeUtils.F90"] + OP["outputDecomposition_m\noutputDecomposition.F90"] + OC["outputCollective_m\noutputCollective.F90"] + OTR["outputTransport_m\noutputTransport.F90"] + OMETA["outputMetadata_m\noutputMetadata.F90"] + OBIN["outputBinary_m\noutputBinary.F90"] + OVIS["outputVisualisation_m\noutputVisualisation.F90"] + end + subgraph PROBES["Probe implementations"] + direction LR + PP["pointProbeOutput_m\npointProbeOutput.F90"] + WP["wireProbeOutput_m\nwireProbeOutput.F90"] + BP["bulkProbeOutput_m\nbulkProbeOutput.F90"] + MP["movieProbeOutput_m\nmovieProbeOutput.F90"] + FP["frequencySliceProbeOutput_m\nfrequencySliceProbeOutput.F90"] + FF["farFieldOutput_m\nfarFieldProbeOutput.F90"] + MV["mapVTKOutput_m\nmapVTKOutput.F90"] + end + end + + subgraph PROJ["Project libraries"] + TYPES["semba-types\nFDETYPES_m"] + REPORTS["semba-reports\nreport_m"] + COMPONENTS["semba-components\nfarfield_m + solver components"] + UTILS["fdtd-utils\nutils_m, allocationUtils_m, directoryUtils_m"] + end + + subgraph EXT["External and conditional dependencies"] + XDMF["XDMF::HDF5\nxdmf_hdf5_m"] + MPI["MPI::MPI_Fortran\nmpi"] + MTLN["MTLN solver\nWire_bundles_mtln_m"] + WIRES["Wire implementations\nHolland / Berenger / Slanted"] + INTRINSIC["Fortran intrinsics\niso_fortran_env"] + VTK["vtkAPI target\nvtkAPI_m"] + end + + OM --> OT + OM --> OU + OM --> DOM + OM --> OP + OM --> OC + OM --> OTR + OM --> OMETA + OM --> PP + OM --> WP + OM --> BP + OM --> MP + OM --> FP + OM --> FF + OM --> MV + OM --> TYPES + OM --> REPORTS + OM --> UTILS + OM -. "CompileWithMTLN" .-> MTLN + + DOM --> OT + OU --> OT + OU --> DOM + OU --> REPORTS + OU --> TYPES + OU --> UTILS + VP --> OT + VP --> OU + VP --> TYPES + VP --> UTILS + + OP --> OT + OP --> TYPES + OC --> OP + OTR --> TYPES + OTR -. "CompileWithMPI" .-> MPI + OMETA --> OT + OMETA --> UTILS + OBIN --> OT + OBIN --> UTILS + OVIS --> OT + OVIS --> UTILS + OVIS --> XDMF + + PP --> OT + PP --> DOM + PP --> OU + PP --> TYPES + PP --> UTILS + WP --> OT + WP --> OU + WP --> TYPES + WP --> UTILS + WP --> WIRES + BP --> OT + BP --> OU + BP --> TYPES + BP --> UTILS + MP --> OT + MP --> OU + MP --> VP + MP --> OBIN + MP --> OMETA + MP --> OVIS + MP --> XDMF + MP --> UTILS + FP --> OT + FP --> OU + FP --> VP + FP --> OBIN + FP --> OMETA + FP --> OVIS + FP --> XDMF + FP --> UTILS + FF --> OT + FF --> OU + FF --> COMPONENTS + FF --> REPORTS + MV --> OT + MV --> OU + MV --> VP + MV --> UTILS + MV --> VTK + MV --> REPORTS + + OT --> TYPES + OT --> XDMF + OT --> WIRES + OT -. "CompileWithBerengerWires / CompileWithSlantedWires" .-> WIRES + OP --> INTRINSIC + OBIN --> INTRINSIC + OVIS --> INTRINSIC + + classDef coordinator fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#111827; + classDef contract fill:#dcfce7,stroke:#16a34a,color:#111827; + classDef adapter fill:#fef3c7,stroke:#d97706,color:#111827; + classDef external fill:#f3e8ff,stroke:#9333ea,color:#111827; + classDef project fill:#e5e7eb,stroke:#374151,color:#111827; + class OM coordinator; + class OT,OU,DOM,VP,OP,OC,OTR,OMETA,OBIN,OVIS contract; + class PP,WP,BP,MP,FP,FF,MV adapter; + class TYPES,REPORTS,COMPONENTS,UTILS project; + class XDMF,MPI,MTLN,WIRES,INTRINSIC,VTK external; +``` + +## Reorganisation Signal + +The dense graph is a useful design signal: + +- `output_m` is a composition root and should remain the only module that + knows every probe implementation. +- `outputTypes_m` is a high-fan-in hub that mixes domain contracts, probe + storage, field containers, wire types, and XDMF writer handles. +- Probe modules combine sampling logic with filesystem, binary, metadata, and + visualisation publication. +- `outputUtils_m` is a second shared hub and currently reaches into domain + types, naming, geometry, and reporting concerns. + +A safer future split is: + +- `output_contracts_m`: artifact, lifecycle, ownership, and metadata contracts. +- `output_probe_types_m`: probe state and solver-facing field containers. +- `output_domain_m`: time, frequency, and spherical sampling domains. +- `output_sampling_m`: coordinate, component, and probe-range calculations. +- `output_publication_m`: small interfaces for text, binary, metadata, and + visualisation publication. +- `output_distributed_m`: partitioning, ownership, transport, and collective + publication. + +The existing probe modules would then depend inward on contracts and sampling, +while publication and MPI implementations depend outward on infrastructure. +This should be introduced incrementally, beginning with extracting contracts +from `outputTypes_m` and moving XDMF and wire-library handles out of that +contract layer. + +## Build-Level Reading + +`fdtd-output` links `semba-components`, `semba-types`, `fdtd-utils`, and +`XDMF::HDF5`. +MPI is added when `SEMBA_FDTD_ENABLE_MPI` is enabled. + +`semba-components` brings in `semba-reports` and the optional MTLN solver. +`fdtd-utils` brings in `semba-types` and owns the filesystem adapter used by +metadata, binary, visualisation, and legacy probe writers. + +`vtkAPI` is a separate target. +The map-VTK implementation uses its module, while the main executable links +both `fdtd-output` and `vtkAPI` through `semba-main`. + +## Source Inventory + +The `fdtd-output` target is defined in `src_output/CMakeLists.txt`. +`vtkAPI` is defined there as a separate library. +The source modules represented above are the files currently listed by that +target, including the lifecycle and publication helpers added for the robust +output layer. diff --git a/doc/output-project-interactions.mermaid.md b/doc/output-project-interactions.mermaid.md new file mode 100644 index 000000000..7b549a506 --- /dev/null +++ b/doc/output-project-interactions.mermaid.md @@ -0,0 +1,112 @@ +# Output Module Project Interactions + +The output module is the runtime boundary between the FDTD solver state and +published simulation artifacts. +The diagram below describes the active new-output path selected by +`CompileWithNewOutputModule`. + +```mermaid +flowchart TD + LAUNCH["launcher.F90\nsemba-fdtd"] --> APP["SEMBA_FDTD_m\nsemba_fdtd_t"] + APP --> PARSE["Input and preprocessing\nSMBJSON / NFDE / Preprocess_m"] + PARSE --> MODEL["Simulation model\nSGGFDTDINFO_t, media, bounds, control"] + MODEL --> SOLVER["Solver_m\ntimestepping.F90"] + + subgraph LIFE["Output lifecycle"] + INIT["init_outputs()\ncreate probe objects and partitions"] + UPDATE["update_outputs()\nsample fields and currents"] + FLUSH["flush_outputs()\nwrite buffered artifacts"] + CLOSE["close_outputs()\nclose XDMF/frequency writers"] + INIT --> UPDATE --> FLUSH --> CLOSE + end + + SOLVER -->|initialise observations| INIT + SOLVER -->|each time step| UPDATE + SOLVER -->|scheduled flush| FLUSH + SOLVER -->|final flush| FLUSH + SOLVER -->|end of run| CLOSE + + FIELDS["FDTD field state\nE/H arrays and time array"] --> UPDATE + CONTROL["sim_control_t\noutput root, rank, cadence, paths"] --> INIT + CONTROL --> UPDATE + CONTROL --> FLUSH + GEOMETRY["Geometry and material state\nmedia, tags, grid steps, PML bounds"] --> INIT + GEOMETRY --> UPDATE + WIRES["Wire / MTLN state\nwire segments and circuit probes"] --> INIT + WIRES --> UPDATE + FARFIELD["farfield_m\nnear-to-far inputs"] --> INIT + FARFIELD --> UPDATE + + INIT --> COORD["output_m\nmanifest, lifecycle, ownership"] + UPDATE --> PROBES["Probe implementations\npoint / wire / bulk / movie / frequency / far field"] + FLUSH --> PROBES + COORD --> PROBES + + PROBES --> BINARY["outputBinary_m\nportable binary records"] + PROBES --> META["outputMetadata_m\nprobe JSON descriptors"] + PROBES --> VIS["outputVisualisation_m\nXDMF + HDF5"] + PROBES --> VTK["mapVTKOutput_m + vtkAPI_m\nVTK geometry"] + PROBES --> TEXT["Human-readable probe files\n.dat and related files"] + + DECOMP["outputDecomposition_m\nrank partitions and offsets"] --> COORD + COLLECTIVE["outputCollective_m\nowner and publication mode"] --> COORD + TRANSPORT["outputTransport_m\nMPI eligibility and gathers"] --> COORD + MPI["MPI runtime\nrank coordination"] --> TRANSPORT + MPI --> COLLECTIVE + COORD --> DECOMP + COORD --> COLLECTIVE + + UTILS["fdtd-utils\ndirectoryUtils, allocation, paths"] --> BINARY + UTILS --> META + UTILS --> VIS + UTILS --> TEXT + XDMF["XDMF::HDF5 external library"] --> VIS + + BINARY --> ARTIFACTS["Run artifacts\n.bin, .dat, .xdmf, .h5, .vtk/.vtu"] + MANIFEST["Root-owned run manifest\n*_output_manifest.json"] + COORD --> MANIFEST + ARTIFACTS --> CONSUMERS["Post-processing and visualisation\nParaview / scripts / users"] + MANIFEST --> CONSUMERS + + CLEANUP["semba_end()\noptional intermediate cleanup"] --> DELETE["delete_run_output_manifest()"] + APP --> CLEANUP + DELETE --> MANIFEST + + subgraph TESTS["Verification boundary"] + UNIT["test/unit/output\nunit and contract tests"] + MPITEST["test/mpi/output\ncollective and root aggregation"] + end + UNIT -. "links and tests" .-> OUTLIB["fdtd-output / vtkAPI"] + MPITEST -. "links and tests" .-> OUTLIB + + LEGACY["Legacy observation dispatch\nFlushObservationFiles / UpdateObservation"] -. "preprocessor fallback" .-> SOLVER + + classDef runtime fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#111827; + classDef output fill:#dcfce7,stroke:#16a34a,color:#111827; + classDef artifact fill:#fef3c7,stroke:#d97706,color:#111827; + classDef external fill:#f3e8ff,stroke:#9333ea,color:#111827; + classDef legacy fill:#fee2e2,stroke:#dc2626,stroke-dasharray:5 5,color:#111827; + class LAUNCH,APP,PARSE,MODEL,SOLVER,FIELDS,CONTROL,GEOMETRY,WIRES,FARFIELD,CLEANUP runtime; + class INIT,UPDATE,FLUSH,CLOSE,COORD,PROBES,DECOMP,COLLECTIVE,TRANSPORT,UNIT,MPITEST,OUTLIB output; + class BINARY,META,VIS,VTK,TEXT,ARTIFACTS,MANIFEST,CONSUMERS,DELETE artifact; + class MPI,UTILS,XDMF external; + class LEGACY legacy; +``` + +## Lifecycle Contract + +Initialisation creates one output object for each supported observation request +and prepares volumetric partitions when needed. +Updates consume the current solver fields and advance probe buffers. +Flushes persist scalar, wire, bulk, far-field, binary, and visualisation data. +The final flush requests far-field completion and then closes long-lived +writers. + +The coordinator also owns output lifecycle metadata, canonical scalar-writer +selection, volumetric publication mode, and the root-owned run manifest. +The manifest is removed by `semba_end()` when intermediate-data cleanup is +requested. + +The legacy branch remains in `timestepping.F90` behind the inverse preprocessor +condition and is explicitly scheduled for retirement in task `T19` of the +robust-output-layer plan. diff --git a/doc/output-upgrades-and-concerns.md b/doc/output-upgrades-and-concerns.md new file mode 100644 index 000000000..e6a5327f5 --- /dev/null +++ b/doc/output-upgrades-and-concerns.md @@ -0,0 +1,93 @@ +# Output Upgrades And Concerns + +This note tracks the remaining work around `src_output`. +The authoritative task breakdown is +`specs/changes/robust-output-layer/tasks.md`. + +## Priority Roadmap + +```mermaid +flowchart LR + CONTRACT["Existing contracts\noutputTypes + lifecycle tests"] --> FS["T10\nportable filesystem and atomic replacement"] + FS --> META["T11\nmetadata publication adapter"] + CONTRACT --> SCALAR["T09\nroute scalar and geometry probes\nthrough declared artifacts"] + CONTRACT --> BIN["T12\nvalidate and integrate binary layout"] + CONTRACT --> VIS["T13\nadapt visualisation writer"] + BIN --> VOLUME["T15/T16\ncomplete movie and frequency outputs"] + VIS --> VOLUME + META --> MANIFEST["T18\nartifact-derived run manifest"] + SCALAR --> MANIFEST + VOLUME --> DIST["T14/T17\ndistributed publication and fallback"] + DIST --> VERIFY["T20/T21/T22\nintegration matrix and CI"] + MANIFEST --> RETIRE["T19\nretire legacy observation dispatch"] + VERIFY --> RETIRE + + classDef current fill:#dcfce7,stroke:#16a34a,color:#111827; + classDef pending fill:#fef3c7,stroke:#d97706,color:#111827; + classDef final fill:#dbeafe,stroke:#2563eb,color:#111827; + class CONTRACT current; + class FS,META,SCALAR,BIN,VIS,VOLUME,DIST,MANIFEST pending; + class VERIFY,RETIRE final; +``` + +## Pending Upgrades + +- **Portable filesystem operations (`T10`).** Replace shell-dependent or + platform-sensitive directory and file handling with native nested-directory + creation and atomic replacement. +- **Complete artifact registration (`T09`, `T11`).** Point, wire, bulk, + far-field, and map-VTK outputs still need consistent declared-artifact + publication and final metadata coverage. +- **Binary contract integration (`T12`, `T15`, `T16`).** The binary writer + exists, but every volumetric path must use it with a validated record size, + precision, byte order, and complex real/imaginary convention. +- **Visualisation integration (`T13`, `T15`, `T16`).** Movie and frequency + probes must reliably produce the complete XDMF/HDF5 pair and register both + artifacts. +- **Distributed publication (`T14`, `T17`, `T21`).** Complete collective + hyperslab publication and root-aggregation fallback, including deterministic + participant order and no duplicate MPI boundary planes. +- **Manifest correctness (`T18`).** Derive the root-owned run manifest from + declared artifacts and remove stale per-rank registration once parity is + proven. +- **Legacy retirement (`T19`).** Remove the old observation dispatch only + after the new output path has equivalent coverage. +- **Verification and CI (`T20` through `T22`).** Add serial, zero-sample, + nested-path, spaced-path, distributed-collective, and distributed-aggregation + coverage across supported build configurations. + +## Current Concerns + +- The output layer currently calls filesystem, metadata, binary, + visualisation, and MPI-related helpers directly from the coordinator and + probe modules. + This makes portability and failure handling cross-cutting rather than + isolated behind adapters. +- `attach_output_partition()` initialises `outputCollective_m` with collective + publication disabled. + The current volumetric path therefore selects root aggregation instead of + exercising the collective XDMF/HDF5 path. +- The metadata writer opens files with `status='replace'`. + A failed write can leave a partial descriptor, so completion must not be + advertised until atomic replacement or an equivalent durability policy is + implemented. +- The output library uses `vtkAPI_m` from `mapVTKOutput.F90`, but `vtkAPI` is a + separate CMake target. + The target dependency should remain explicit so module-generation and link + ordering cannot depend on incidental build behaviour. +- `timestepping.F90` still contains both the new and legacy output branches. + Any change to flush cadence or finalisation must be checked against both + paths until `T19` is complete. +- Root aggregation is a correctness fallback, not a scalable default. + Large volumetric probes can require root memory proportional to the complete + requested volume. +- The output contract supports zero-sample probes, but end-to-end tests must + verify that descriptors and required artifact declarations still exist when + no samples are written. + +## Related Diagrams + +- [Module dependencies](output-module-dependencies.mermaid.md) +- [Project interactions](output-project-interactions.mermaid.md) +- [Robust output-layer plan](../specs/changes/robust-output-layer/plan.md) +- [Output metadata schema](../specs/changes/robust-output-layer/contracts/output-metadata-schema.md) diff --git a/doc/output.md b/doc/output.md new file mode 100644 index 000000000..4e8dace0c --- /dev/null +++ b/doc/output.md @@ -0,0 +1,26 @@ +# Binary Probe Output + +Every sampled probe publishes a binary artifact beside its existing text result. +Artifact metadata declares the artifact path, byte order, numeric representation, +record size, component order, and complex-value representation. + +Binary values use little-endian IEEE real64 records. +Each record uses the highest native precision required by its coordinate and +measured values. +Empty binary artifacts are created when their probe is initialised. + +Time scalar records contain `time,value`. +Frequency scalar records contain `frequency,value.real,value.imag`. +Bulk and line records use the time scalar layout. +Wire-current records contain time followed by current and voltage values. +Wire-charge records contain `time,charge`. + +Volumetric time records contain time, three coordinates, and three values. +Volumetric frequency records contain frequency, three coordinates, and real and +imaginary values for each vector component. +Far-field records contain frequency, theta, phi, field magnitudes and phases, +and arithmetic and geometric radar-cross-section values. + +Binary and text results describe the same samples in the same order. +Comparisons against text results use a tolerance matching the text format's +published precision. 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..5e5232110 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,43 +1,61 @@ 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 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 + working_dir: /home/developer/workspaces/fdtd + 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..13e0cfd0b 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -25,4 +25,8 @@ 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() + +if (SEMBA_FDTD_ENABLE_HDF) + add_subdirectory("xdmf-hdf5/") +endif() diff --git a/external/xdmf-hdf5/CMakeLists.txt b/external/xdmf-hdf5/CMakeLists.txt new file mode 100644 index 000000000..c4aaccaa9 --- /dev/null +++ b/external/xdmf-hdf5/CMakeLists.txt @@ -0,0 +1,125 @@ +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) +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..1e19c7f0e --- /dev/null +++ b/external/xdmf-hdf5/src/xdmf_hdf5.F90 @@ -0,0 +1,2590 @@ +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 + 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) + 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 + 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) + 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 + 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) + 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 + 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) + 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 (.not. this%attributes(index)%is_series) then + call set_status_error(status, XDMF_ERROR_STATE, & + 'Collective hyperslabs currently support series attributes only') + 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 (.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 + 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..0620895e6 --- /dev/null +++ b/external/xdmf-hdf5/src/xdmf_hdf5_backend.F90 @@ -0,0 +1,1135 @@ +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 + +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 + 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 + 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) + 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(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 + + buffer_dims(1) = int(size(data), HSIZE_T) + call prepare_full_transfer(file, dataset_id, buffer_dims, filespace, & + memspace, status) + if (status%is_error()) then + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + -1, status) + return + end if + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL32), & + data, buffer_dims, error, memspace, filespace, file%transfer_property) + 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) + 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(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 + + buffer_dims(1) = int(size(data), HSIZE_T) + call prepare_full_transfer(file, dataset_id, buffer_dims, filespace, & + memspace, status) + if (status%is_error()) then + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + -1, status) + return + end if + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_REAL64), & + data, buffer_dims, error, memspace, filespace, file%transfer_property) + 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) + 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(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 + + buffer_dims(1) = int(size(data), HSIZE_T) + call prepare_full_transfer(file, dataset_id, buffer_dims, filespace, & + memspace, status) + if (status%is_error()) then + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + -1, status) + return + end if + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT32), & + data, buffer_dims, error, memspace, filespace, file%transfer_property) + 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) + 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(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 + + buffer_dims(1) = int(size(data), HSIZE_T) + call prepare_full_transfer(file, dataset_id, buffer_dims, filespace, & + memspace, status) + if (status%is_error()) then + call finish_fixed_write(file, dataset_id, filespace, memspace, path, & + -1, status) + return + end if + call h5dwrite_f(dataset_id, hdf_datatype(XDMF_NUMERIC_INT64), & + data, buffer_dims, error, memspace, filespace, file%transfer_property) + 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) + 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 :: 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. 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, '