pyopentrustregion 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyopentrustregion-3.0.0/.clang-format +2 -0
- pyopentrustregion-3.0.0/.codecov.yml +3 -0
- pyopentrustregion-3.0.0/.gitignore +27 -0
- pyopentrustregion-3.0.0/CLAUDE.md +221 -0
- pyopentrustregion-3.0.0/CMakeLists.txt +400 -0
- pyopentrustregion-3.0.0/LICENSE +373 -0
- pyopentrustregion-3.0.0/PKG-INFO +548 -0
- pyopentrustregion-3.0.0/README.md +524 -0
- pyopentrustregion-3.0.0/cmake/OpenTrustRegionConfig.cmake.in +138 -0
- pyopentrustregion-3.0.0/include/opentrustregion.h +241 -0
- pyopentrustregion-3.0.0/pyopentrustregion/__init__.py +16 -0
- pyopentrustregion-3.0.0/pyopentrustregion/python_interface.py +1151 -0
- pyopentrustregion-3.0.0/pyopentrustregion/test_data/h2o_atomic_mo_coeff.bin +0 -0
- pyopentrustregion-3.0.0/pyopentrustregion/test_data/h2o_minimum_mo_coeff.bin +0 -0
- pyopentrustregion-3.0.0/pyopentrustregion/test_data/h2o_r2_ints.bin +0 -0
- pyopentrustregion-3.0.0/pyopentrustregion/test_data/h2o_r_ints.bin +0 -0
- pyopentrustregion-3.0.0/pyopentrustregion/test_data/h2o_saddle_mo_coeff.bin +0 -0
- pyopentrustregion-3.0.0/pyopentrustregion/testsuite.py +2325 -0
- pyopentrustregion-3.0.0/pyopentrustregion/verify_install.py +73 -0
- pyopentrustregion-3.0.0/pyproject.toml +91 -0
- pyopentrustregion-3.0.0/src/c_interface.f90 +1111 -0
- pyopentrustregion-3.0.0/src/opentrustregion.f90 +4175 -0
- pyopentrustregion-3.0.0/tests/c_integration_tests.c +1046 -0
- pyopentrustregion-3.0.0/tests/c_integration_tests.h +28 -0
- pyopentrustregion-3.0.0/tests/c_interface_mock.f90 +332 -0
- pyopentrustregion-3.0.0/tests/c_interface_unit_tests.f90 +2278 -0
- pyopentrustregion-3.0.0/tests/opentrustregion_mock.f90 +268 -0
- pyopentrustregion-3.0.0/tests/opentrustregion_system_tests.f90 +719 -0
- pyopentrustregion-3.0.0/tests/opentrustregion_unit_tests.f90 +10727 -0
- pyopentrustregion-3.0.0/tests/test_reference.f90 +2051 -0
- pyopentrustregion-3.0.0/tools/f90_layout.py +2115 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
|
|
4
|
+
# IDEs
|
|
5
|
+
.vscode/
|
|
6
|
+
|
|
7
|
+
# Libraries
|
|
8
|
+
*.so
|
|
9
|
+
*.dylib
|
|
10
|
+
|
|
11
|
+
# Executables
|
|
12
|
+
testsuite
|
|
13
|
+
|
|
14
|
+
# Module files
|
|
15
|
+
*.mod
|
|
16
|
+
|
|
17
|
+
# OS files
|
|
18
|
+
.DS_Store
|
|
19
|
+
|
|
20
|
+
# Distribution / packaging
|
|
21
|
+
build/
|
|
22
|
+
dist/
|
|
23
|
+
wheelhouse/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
|
|
26
|
+
# Environment files
|
|
27
|
+
.python-version
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
OpenTrustRegion is a Fortran library implementing a second-order trust-region optimizer, exposed via Fortran, C, and Python (ctypes) interfaces — the same compiled shared/static library is consumed by all three.
|
|
8
|
+
|
|
9
|
+
## General conventions
|
|
10
|
+
|
|
11
|
+
**Formatting.** Fortran lines max 88 columns. Layout, spacing, keyword spelling, string literals, comments and blank lines of Fortran code are defined by `tools/f90_layout.py`, whose docstring is the authoritative rule list; don't restate its rules here or apply them by hand. Run `python3 tools/f90_layout.py` after every Fortran edit: by default it checks only statements touched relative to `HEAD`, `--fix` applies the fixes, and `--all` checks whole files (with `--fix` it requires explicit paths, e.g. `git ls-files '*.f90' | xargs python3 tools/f90_layout.py --all --fix`). CI runs `--all` on every tracked `.f90` file. The default mode also checks every untracked, non-ignored `.f90` file in full, so a bare `--fix` rewrites scratch copies of Fortran files left in the tree too; move them out first. Statements containing a comment, a `;` or a continuation line starting with `&` are never reflowed (their spacing and indentation are still fixed), so shorten an overlong one by hand. Every procedure opens with a `!`-delimited comment block describing what it does; each logical step inside gets a lowercase `!` comment. Unit tests: assume success (`test_<name> = .true.`), then one `if (...) then` / `write(stderr, *) "test_<name> failed: ..."` / `test_<name> = .false.` block per assertion, no early returns except where a later assertion would crash or has nothing to check because a call it depends on already failed. Compare a value with a finite reference through `is_close` from `test_reference.f90`, as `.not. is_close(x, ref)` or `.not. all(is_close(x, ref))`, with a tolerance other than `tol` as its third argument, `0.0_rp` for exact agreement, and check a bound through `is_below`, `is_at_most`, `is_above` or `is_at_least`, as `.not. is_at_most(x, bound)`, also for a value that has to vanish exactly (`is_at_most(abs(x), 0.0_rp)`), with the relation of the criterion the bound mirrors (`is_below(grad_rms, conv_tol)` for the solver's convergence): these fail for a value that is not a number or infinite, whereas `abs(x - ref) > tol` or `x > bound` let a NaN pass, since every comparison with a NaN is false. Python compares through `np.isclose` and C and Python write their few bounds as the negated condition (`not x < 0.0`), both of which a NaN fails too.
|
|
12
|
+
|
|
13
|
+
**Python formatting.** All Python (`pyopentrustregion/`, `tools/`) is `black`-formatted, default settings. Run `black pyopentrustregion tools` before considering Python changes done.
|
|
14
|
+
|
|
15
|
+
**C formatting.** All C headers and sources (every tracked `.c` and `.h` file, in `include/`, `tests/` and `.github/consumer/`) are `clang-format`-formatted per the repo-root `.clang-format` (LLVM style, 88-column limit to match the Fortran convention above). Run `clang-format -i` on touched C/H files before considering C changes done.
|
|
16
|
+
|
|
17
|
+
**Clarity over performance.** The library is not on the hot path of a quantum-chemistry calculation — the host program's integral transforms and Hessian-vector products dominate. Prefer short, obviously-correct code over fast code. Don't propose performance refactors (buffer growth, pooling, micro-optimizations) without evidence the affected code is hot for a real workload.
|
|
18
|
+
|
|
19
|
+
**Norms: BLAS in production, intrinsics in tests.** Production Fortran (`src/`) uses BLAS (`dnrm2`, `ddot`) for norms/dot products. Test code (`tests/`) uses intrinsics (`norm2`, `dot_product`, `matmul`, `sum`, `transpose`) instead. BLAS/LAPACK is allowed in tests only where no intrinsic exists (`dsyev`, `dgeev`, `zheev`) — don't hand-roll linear algebra to avoid the dependency.
|
|
20
|
+
|
|
21
|
+
### Unit tests must not depend on untested code
|
|
22
|
+
|
|
23
|
+
A unit test may only call the routine it is testing — never another production routine to build its inputs or expected values, or the test silently inherits that routine's correctness.
|
|
24
|
+
|
|
25
|
+
- Construct inputs directly in the test, randomly or with LAPACK, not by calling the production routine that would normally produce them.
|
|
26
|
+
- When a routine under test internally calls another module's routines, reimplement those as `ref_*` helpers so the expected value is computed independently.
|
|
27
|
+
- Where a routine merely delegates, assert the *contract* — that it hands its inputs to the callee and passes on what the callee returned — rather than re-deriving numbers a different routine's own test already covers.
|
|
28
|
+
- Exercise only the options the routine itself distinguishes. Where it calls another production routine, its test checks only that the callee was called (with the right arguments, and that its result is used correctly), not the callee's options or the input regimes that drive the callee down different branches; those belong in the callee's own test. E.g. `test_solver` checks that the solver, with `line_search` set, hands the step to `bracket`, while which bracketing branch an objective function takes is covered in `test_bracket`.
|
|
29
|
+
|
|
30
|
+
### Test code layout
|
|
31
|
+
|
|
32
|
+
Test code is organized by role, one file each:
|
|
33
|
+
|
|
34
|
+
| File | Role |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `test_reference.f90` | Tolerances, the comparison `is_close` within them and the bounds `is_below`, `is_at_most`, `is_above` and `is_at_least`, reference values, and `ref_*` reimplementations computed independently of the routine under test. The reference settings `ref_solver_settings`/`ref_stability_settings` are values of the production types with a distinct value in every field but the logicals, which the tests set one at a time instead, and the callbacks and context, which `get_reference_*_values` sets to distinct addresses; C and Python get them field by field without the conversion routines under test, as a filled C struct (`get_reference_solver_values`) or by field name (`reference_field`), so their tests hold no values of their own. The reference states `ref_solver_state`/`ref_stability_state` likewise have a value in every field that is distinct from every other field of the same kind and from its default, exported as a filled C struct (`get_reference_*_state_values`) or by field name (`reference_state_field`); `arm_state_c` lets a callback bundle refer to a state and a host's record holding other values, so that the C mocks detect whether the state was copied into the record before they were called. `callbacks_unset`, `unset_callbacks` and `callbacks_wrapped` check, unset or check the wrapping of every callback function of Fortran or C settings, so a new callback function is listed there once instead of in every test. The checkers `check_<callback>_funptr`/`check_<callback>_c_funptr`, which the mocks and the `*_f_wrapper` tests use to check the function pointers they receive, sit side by side as Fortran/C pairs for every callback that returns values or an error code; the logger has none and is called directly. Also the host-context fixtures shared by both unit suites: `host_context_type` with its `host_context` instance, `arm_host_context`/`arm_host_context_c`, `check_host_context`/`check_host_context_c` and `host_context_reached`, `host_context_error`, through which the C mocks report an error set in the host context, the `bind(C)` checks `is_default_solver_settings`/`is_default_stability_settings` that the C `*_init()` tests use, and the warnings the C entry points print for settings that were not initialized, as null-terminated strings for the C integration tests, whose capture of the standard error flushes what the Fortran runtime buffered there through `flush_fortran_stderr`, since it buffers the standard error while that is a file. |
|
|
37
|
+
| `opentrustregion_unit_tests.f90` | The Fortran-level `test_*` functions, plus mocks/fixtures needed only to drive them, and the `hartmann6d_*` `bind(C)` data of the Hartmann 6D problem that the C and Python integration tests read. |
|
|
38
|
+
| `opentrustregion_mock.f90` | Mocks of the module's own production routines that are bridged to the C interface (`solver`, `stability_check`), so a test can verify a routine invoked them correctly without running the real logic. |
|
|
39
|
+
| `c_interface_unit_tests.f90` | Tests for the `bind(C)` wrapper layer itself; defines its own local `bind(C)` mocks rather than using `c_interface_mock.f90`. |
|
|
40
|
+
| `c_interface_mock.f90` | `bind(C)`-signature mocks used exclusively by the Python interface unit tests, dynamically loaded via `ctypes` from `libotrtestsuite`. |
|
|
41
|
+
| `opentrustregion_system_tests.f90` | The system tests on the water Foster-Boys localization, see below. |
|
|
42
|
+
| `c_integration_tests.c` | The C integration tests through the header, see below. What the file provides to others is declared in its header (`c_integration_tests.h`), which the file itself includes, so that the compiler checks the definitions against the declarations its users see, since C links by name only. |
|
|
43
|
+
|
|
44
|
+
**`ref_*` naming.** The prefix marks a role — an independent reimplementation of what a production routine computes — not a location; it travels with the function wherever the placement rule moves it, and it is decided by what the helper computes, not by whether a call site compares against it or feeds it in as an input. A fixture that builds a *plausible* input (randomly, via LAPACK, or by reassembling a routine's own output) does not take the prefix, even in the same file. Either kind may keep an atypical name purely to dodge a collision with a production routine or local variable in scope at its call site — check for a collision before insisting on the "cleaner" name.
|
|
45
|
+
|
|
46
|
+
**Module-level `use` in test files is restricted.** In `<name>_unit_tests.f90` and siblings, module-level `use` statements above `contains` are limited to `rp`, `ip`, their C counterparts `c_rp` and `c_ip`, `kw_len`, `stderr`, `stdout`, `tol`, the comparison `is_close` and the bounds `is_below`, `is_at_most`, `is_above` and `is_at_least`, the dimension `n_param`/`n_param_c` and the reference settings `ref_solver_settings`/`ref_stability_settings` (with the operators comparing against them) that `test_reference.f90` fixes for the C-interface fixtures, and intrinsic module bindings (`iso_c_binding` and the like) — check other modules for the exact set before adding to it. Names from `iso_c_binding` are always imported at module level, never inside a procedure. The exceptions are what a declaration in the module's specification part needs: the parent of a test-local type extension, since a type with type-bound procedures can only be declared there, the interface of a procedure pointer that checks a mock complies with it (`procedure(update_orbs_c_type), pointer :: mock_update_orbs_ptr => mock_update_orbs`), the type of a variable in which a mock records what it received (`received_stability_callbacks` in `opentrustregion_mock.f90`), and the type of a component of a test-local type or of a module-level fixture (`solver_state_type` for `double_well_state_context_type`, the mock states in `opentrustregion_mock.f90`, `armed_state_c` in `test_reference.f90`). Everything else (mock callbacks, `setup_settings`, other shared dimension parameters, `ref_*` helpers) is imported locally inside the specific `test_*` function that needs it, even if several functions in the file need the same symbol. A module-level `use` beyond this set pollutes every procedure in the file and hides which test actually relies on what.
|
|
47
|
+
|
|
48
|
+
**Prefer shared dimension-parameter fixtures over local literals**, unless it complicates the test a lot. Where a canonical dimension exists for the data a test works on, import it rather than redeclaring it: the core unit tests' `n_param` is the dimension of the Hartmann 6D problem most of them share, defined with that problem in `opentrustregion_unit_tests.f90`, and `test_reference.f90`'s `n_param` is the dimension of the C-interface fixtures.
|
|
49
|
+
- A test whose local variable already carries the name renames the import on its `use` line only, with the suffix `_ref`, never the prefix `ref_`, which marks independent reimplementations.
|
|
50
|
+
- Exception: a test whose expected values are hand-derived for a specific small/structured case (a closed-form 2×2 rotation, 3×3 matrices with hand-computed results, a matrix crafted to trigger a rank-detection path) may keep local literals and declare its own dimensions, since forcing the shared size would mean re-deriving the numbers. When correctness is checked via algebraic properties rather than exact values, use the shared fixture.
|
|
51
|
+
|
|
52
|
+
**Prefer generated data over hand-typed literals** (`generate_random_symm_matrix` / `call random_number`), unless the specific values are load-bearing. A hand-typed `reshape([...])` is justified only when the test checks a specific hand-derivable closed-form target depending on those exact numbers. When every assertion is relational (a copy, a fixed scaling, an algebraic identity, or a value the test recomputes independently from the same input) the specific values never mattered, and random generation exercises the same code path while being harder to accidentally pass with a bug. Before hand-typing a matrix meant to be "just some valid state" (often signaled by the test calling it "arbitrary"), check whether random generation works instead. Where a matrix must stay block-diagonal/diagonal/structurally constrained to match what production produces, keep that structure but randomize within it.
|
|
53
|
+
|
|
54
|
+
### Shared architectural patterns
|
|
55
|
+
|
|
56
|
+
**Settings types.** `solver_settings_type` / `stability_settings_type` extend the abstract `settings_type`, and every component of these types has a default initializer, so that a declared settings object, like an `intent(out)` dummy, holds the default settings; a component the solver and the stability check share but whose defaults differ (`conv_tol`, `n_random_trial_vectors`, `jacobi_davidson_start`) is declared in each of the two types rather than in `settings_type`, since an extended type cannot change the default of a parent component. Default values are thereby defined once, at the components, with one exception: the stability `n_random_trial_vectors` defaults to a negative value meaning automatic, which `init_defaults` resolves to 1 for the solver's internal check and `stability_sanity_check` to `standalone_n_random_trial_vectors` (20) for a standalone one, so that the C and Python defaults show −1; `init_defaults` likewise switches on the internal check's `stop_on_instability` for a symmetric Hessian without a nested `conv_check`. The `default_*_settings` parameters are the structure constructors without arguments, and the C `*_init()` helpers and the Python `Settings` wrappers obtain the defaults by calling `init_*_settings_c` (C name `init_*_settings`) in `src/c_interface.f90`. The settings are inputs only: `solver` and `stability_check` take them `intent(in), optional` and work on a local copy (`local_settings`), of the default settings when none are passed, so the caller's settings are never written; only the default resolution `init_defaults` and the sanity checks write the copy, and every other internal routine takes it `intent(in)`. What the core reports lives in the states (below). C structs cannot be default-initialized, so the C settings keep an `initialized` flag, which their `*_init()` sets; C settings that arrive uninitialized anyway are replaced by the defaults in the `assign_*_f_c` conversion, and since such settings carry no host context, `store_optional_c_callbacks` skips their optional callbacks and context. The C entry points print a warning for them regardless of the verbosity, on stderr for uninitialized settings, which provide no logging function, and for uninitialized nested settings through the solver's logging function, or on stderr without one. The C entry points take `const` settings pointers that may be `NULL` and Python `settings=None`, both meaning the defaults; without settings, the C wrappers work on a copy of the default settings (`local_settings_c`) rather than passing none on, since the Fortran settings carry the callback bundle in their context.
|
|
57
|
+
|
|
58
|
+
**States.** What the core reports lives in `solver_state_type`/`stability_state_type`, plain records with default initialization and no `bind(C)`, which `solver` and `stability_check` take as an optional `intent(out), target` argument right after the settings and keep current in place through the pointer `current_state` at the caller's record or a local one, so that a host reaching the record through a pointer reads the current values during a callback. The solver's state nests that of its most recent internal stability check as `stability_state`. The internal routines that update the state while calling callbacks take it as a `target` dummy, those that only count calls of a callback a `target` counter. The C interface has `bind(C)` twins ordered by kind (logicals, reals, integers, nested), converted by `assignment(=)`. The C entry points take a state pointer after the settings (`NULL` for none), and the callback bundles refer to the Fortran state and the host's record; `copy_state_to_c` copies the former into the latter in every callback wrapper right before the call into C and when the call returns, also after an error. Python's `SolverState`/`StabilityState` expose the fields of their ctypes structs as read-only properties (`bind_state_fields`). Neither the settings nor the state structs carry a size or version member: a C program is rebuilt against the header of the library it links, like a Fortran program against its modules, for every minor version, which the library's `SOVERSION` (major.minor) and the CMake package's `SameMinorVersion` compatibility enforce, so a settings or state field may change in a minor version but never in a patch version.
|
|
59
|
+
|
|
60
|
+
**The host context.** Every callback interface takes `class(*), intent(in), pointer :: context` as its last dummy (`void *context` in C), and `settings_type` carries a matching `class(*), pointer :: context` component. The library passes `settings%context` at every call site unconditionally — a *pointer* dummy accepts a disassociated actual, which is why no call site branches on `associated`, and why the component must be `pointer` rather than `allocatable` (the internals declare `settings` `intent(in)`, which protects the pointer association but not the target). `init_defaults` lets the nested `stability_settings` of the solver's copy inherit its context when that is unset, together with its preconditioning, projection and logging functions. When the nested context *is* set it wins: the internal check hands it to every callback it calls, including the `hess_x` returned by the solver's `update_orbs` and the inherited `precond`/`project`/`logger`, so a host that sets one must make those callbacks accept it (documented in the README). `test_init_defaults` checks that nested settings without a context or callbacks of their own inherit the solver's and that their own are kept, `test_check_stationary_point` that the nested context reaches the callbacks of the internal stability check, `test_stability_check` that every callback receives `settings%context`, and `test_solver_c_wrapper` that the C wrapper's nested bundle holds the nested settings' own callbacks and context where they were provided and initialized and the solver's otherwise.
|
|
61
|
+
|
|
62
|
+
**The Python → C → Fortran call chain:** `solver(...)` in `python_interface.py` copies the `SolverSettingsC` ctypes struct a `SolverSettings` object wraps (`NULL` without settings), writes the `CFUNCTYPE` wrappers of the Python callbacks into the copy and keeps them alive until the call returns, so that, as in C and Fortran, a call never writes the settings object, takes the `SolverStateC` struct of a `SolverState` (`NULL` without a state), and calls into `libopentrustregion`'s C entry point (`src/c_interface.f90`), which collects the C function pointers plus the host's `void*` and, with a state, pointers to the Fortran state and the host's record in a `c_callbacks_type` bundle on the entry point's own stack frame, stores that bundle in `settings%context`, and invokes `standard_solver` (the real `solver`) with Fortran-shaped wrappers that `select type` the bundle back out. **Consequence:** there is no module-level callback state, so solves nest and run concurrently, since `CMakeLists.txt` compiles every procedure for recursion (`-frecursive` for gfortran, `-recursive` for Intel, Flang's default), which a nested call needs because it re-enters procedures that are still running; without the flag, a gfortran Debug build stops a nested solve at its runtime recursion check (`Recursive call to nonrecursive procedure`), which the solve nested in `test_solver_c` therefore detects. The one piece of process-wide state left is the Fortran runtime's random number generator, which `init_rng` reseeds on every `solver`/`stability_check` call, so a nested or concurrent call perturbs the other's random trial vectors, not its correctness. The internal stability check gets a *second* bundle that starts as a copy of the solver's (so inherited callbacks resolve) and is then overridden by whatever `settings_c%stability_settings` provides — a nested bundle whose slot is empty while the Fortran pointer is inherited would call a null procedure pointer. Python needs no context: `python_interface.py` accepts arbitrary callables, so closures already carry host data.
|
|
63
|
+
|
|
64
|
+
## Build & test
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
# Fortran/C build
|
|
68
|
+
mkdir build && cd build
|
|
69
|
+
cmake .. # add -DBUILD_SHARED_LIBS=ON for shared
|
|
70
|
+
cmake --build .
|
|
71
|
+
|
|
72
|
+
# Python install (scikit-build-core drives CMake, see pyproject.toml; CMake options
|
|
73
|
+
# are passed as -C cmake.define.<OPTION>=<VALUE>)
|
|
74
|
+
pip install .
|
|
75
|
+
pip install -e . # editable
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
# full suite (Python driver calling into libotrtestsuite: unit, integration and system tests)
|
|
80
|
+
python3 -m pyopentrustregion.testsuite # from an installed/editable build, run outside the checkout
|
|
81
|
+
python3 pyopentrustregion/testsuite.py # from the source tree against ./build
|
|
82
|
+
|
|
83
|
+
# quick check that an installed library solves correctly (works against a published
|
|
84
|
+
# wheel, which has no libotrtestsuite and therefore cannot run the testsuite above)
|
|
85
|
+
python3 -m pyopentrustregion.verify_install
|
|
86
|
+
|
|
87
|
+
# single test class or method (stdlib unittest)
|
|
88
|
+
python3 -m unittest pyopentrustregion.testsuite.OpenTrustRegionSystemTests
|
|
89
|
+
python3 -m unittest pyopentrustregion.testsuite.OpenTrustRegionUnitTests.test_solver
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The Python suite runs Fortran- and C-side tests (via symbols dynamically loaded from `libotrtestsuite`) and pure-Python wrapper tests. System tests need `pyopentrustregion/test_data/*.bin`.
|
|
93
|
+
|
|
94
|
+
**Where to add coverage for a new setting.** As assertions inside the existing unit test for the routine the setting affects (`tests/opentrustregion_unit_tests.f90`), not as a new system test — system tests exercise real chemistry data end-to-end, and one per settings combination would explode combinatorially. When a setting can push a routine down a materially different code path (interior vs. boundary-crossing branch in a trust-region solver), exercise both — a check that only hits the "easy" branch (e.g. always starting near a minimum) can pass while a branch reached only near a saddle point stays untested.
|
|
95
|
+
|
|
96
|
+
**What the system and integration tests cover.** The tests come in three tiers: unit tests run one routine against mocks, integration tests run the real library through one interface's declarations, and system tests run the real library on real data. Each tier checks what it adds, not the scenarios of the tier below. The system tests check numerical behaviour:
|
|
97
|
+
|
|
98
|
+
| Test | Covers |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `test_h2o_fb_solver_<option>` (`opentrustregion_system_tests.f90`) | `solver` on the water Foster-Boys localization with the default settings and with one algorithm-selecting option switched on at a time (the subsystem solvers `davidson_ah`, `jacobi-davidson_ls`, `jacobi-davidson_ah` and `tcg`, `line_search`, `stability`, and `hess_symm` switched off, which takes the non-symmetric path of the default solver), each from a guess and from a saddle point, the Jacobi-Davidson solvers switching to Jacobi-Davidson right away; checks the converged gradient, the reference minimum and its stability. |
|
|
101
|
+
| `test_h2o_fb_stability_check_<option>` (same file) | `stability_check` with the default Davidson, with the Jacobi-Davidson diagonalization solver and with `hess_symm` switched off, which takes the non-symmetric path of the Davidson solver, at the minimum (stable) and at a saddle point (unstable, returned direction along the eigenvector of the lowest Hessian eigenvalue, since the saddle point has several unstable modes). |
|
|
102
|
+
|
|
103
|
+
The integration tests check what crosses the C and Python interfaces. The interface unit tests replace the other side with mocks, so these are the only tests that run the real entry points behind the real header and ctypes declarations; their solves on the Hartmann 6D problem only serve as evidence that the results came back:
|
|
104
|
+
|
|
105
|
+
| Test | Covers |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `test_solver_c`, `test_stability_check_c` (`c_integration_tests.c`) | The real library through the C header: every optional callback of both settings structs, including the nested stability settings' own, the host context reaching every callback (including those of the internal stability check, whose nested bundle carries the host's pointer), a stability check and a solve nested in a running solve, the returned states and the states current in the callbacks, the stability flag, the direction and the lowest eigenvalue the state holds, the call without a direction, and the warnings the entry points print on the standard error for settings and nested stability settings that were not initialized, which the tests capture by redirecting it, and their absence without settings. |
|
|
108
|
+
| `test_settings_layout` (same file, and `PyIntegrationTests` for the ctypes structures in `python_interface.py`) | That every field of the C settings structs, including the nested stability settings, is read back under its own name from settings whose fields Fortran sets one by one to the reference values (`get_reference_solver_values` in `test_reference.f90`), compared against the value `reference_field` returns for the field's name, and, in C, that the structs have the size of the `bind(C)` types (`solver_settings_size`/`stability_settings_size`, which the library exports), so any drift between the header and the `bind(C)` types is caught, including a field missing from the end of a struct, except a field left in the C header that fits into the padding of the `bind(C)` type, which changes neither the size nor the offset of a field the test names and which the C test cannot see, since C cannot enumerate the fields of a struct. The sizes of the ctypes structures are checked by `python_interface.py` itself when it is imported (`check_struct_sizes`). The Python test loops over the ctypes fields, so it also catches a stray ctypes field, and a new field needs no change there. |
|
|
109
|
+
| `test_state_layout` (same file, and `PyIntegrationTests` for the ctypes structures in `python_interface.py`) | That every field of the C state structs, including the nested stability check state, is read back under its own name from a state whose fields Fortran sets one by one to the reference values (`get_reference_solver_state_values`), compared against the value `reference_state_field` returns for the field's name, and, in C, that the structs have the size of the `bind(C)` types (`solver_state_size`/`stability_state_size`), which the Python interface checks at import. |
|
|
110
|
+
| `test_solver_settings_init`, `test_stability_settings_init` (same file) | That the header's inline `*_init()` wrappers return the library's defaults with no callbacks and no host context. Both the initialization and the comparison access the struct's fields in Fortran, so these do not detect layout drift; `test_settings_layout` does. |
|
|
111
|
+
| `test_solver_py`, `test_stability_check_py` (`PyIntegrationTests` in `testsuite.py`) | The real library through the Python wrapper: every optional callback of both settings classes, including the nested stability settings' own, the returned states and the states current in the callbacks, the stability flag, the direction and the lowest eigenvalue the state holds, and the call without a direction. |
|
|
112
|
+
|
|
113
|
+
System tests are registered per option and integration tests per interface scenario, not per routine: the one-registered-test-per-routine rule and the advice above to cover a new setting in the unit tests apply to unit tests and to settings that only tune a routine. A new option that selects a different algorithm gets its own registered `test_h2o_fb_solver_<option>` or `test_h2o_fb_stability_check_<option>` test, never a combination with another non-default option. The integration tests gain a check only when a new callback, settings or state field, return value or message on the standard error crosses their interface.
|
|
114
|
+
|
|
115
|
+
### Registering a new Fortran unit test
|
|
116
|
+
|
|
117
|
+
A `logical(c_bool) function test_<routine>() bind(C)` is only reachable once wired into both:
|
|
118
|
+
|
|
119
|
+
1. The source file in the CMake test source list (`OPENTRUSTREGION_TESTS`).
|
|
120
|
+
2. The `fortran_tests["opentrustregion_unit_tests"]` (or `"c_interface_unit_tests"`) name list in `pyopentrustregion/testsuite.py`, alphabetically ordered, without the `test_` prefix — the `@add_tests` decorator on the corresponding `unittest.TestCase` turns each name into a `test_<name>` method.
|
|
121
|
+
|
|
122
|
+
A name in the Python list with no matching Fortran symbol fails as that test with `AttributeError: dlsym(...): symbol not found` when it runs.
|
|
123
|
+
|
|
124
|
+
System tests and C integration tests are wired the same way: their source files are in the same CMake list, and their names go into `fortran_tests["opentrustregion_system_tests"]` and `fortran_tests["c_integration_tests"]`.
|
|
125
|
+
|
|
126
|
+
**Exactly one registered test per production routine** — never `test_<routine>_<case_a>`, `test_<routine>_<case_b>`. Cover multiple configurations as cases *inside* that one test: loop where setup can be shared, or call a per-case `check_*` helper that is deliberately not `bind(C)` and not registered. Keep the case name in the failure message (`"test_<routine> failed: ... for <case>."`) since the harness only reports the registered name.
|
|
127
|
+
|
|
128
|
+
Because a routine's cases live behind one entry, a silently-skipped case is invisible in suite output — have the driving test run every case unconditionally and combine the results without short-circuiting, rather than returning early on the first failure. Write `if (.not. check_case(...)) test_x = .false.`, never `test_x = test_x .and. check_case(...)`: gfortran skips the call in the latter once `test_x` is false in optimized builds, so the remaining cases neither run nor report.
|
|
129
|
+
|
|
130
|
+
**Check that an error message was printed, not its text.** When the routine under test detects an error itself, assert the error code and, separately, that an error message was printed, with only error messages passed to the logger and the log cleared beforehand (`setup_error_logging` in `opentrustregion_unit_tests.f90`). Since the routine returns right after printing it, a non-empty log can only be that message; don't compare its text. An error a routine merely propagates from a failing callback prints nothing, so there is no message to check. Warnings are compared by their exact text: the routine continues after a warning, so other messages can follow it in the same log. Compare against the `*_warning_msg` parameter the production module defines for each warning instead of repeating its text in the test.
|
|
131
|
+
|
|
132
|
+
**Error propagation is covered by fault injection.** A routine's own test checks the origin an error is reported with at its own callback call sites (`error_hess_x + 1` for a failing Hessian linear transformation it calls) and on an error an internal callee detects in the routine's own arguments (`error_stability_check + 1` for settings the sanity check rejects), not the `if (error /= 0) return` lines that only pass on an error of a failing callback through an internal callee. Those are covered by the fault-injection sweeps in `test_solver` and `test_stability_check`: an unfaulted run counts every callback call in a `hartmann6d_fault_context_type`, then every one of these calls fails in turn, with an error and, where the callback returns values, with a NaN (`fault_kind`), and the test checks that the error is reported with the failing callback's origin, the NaN as an error of the solver or the stability check, unless it is an objective function value, that no callback is called after the failing call and that the reported counters still agree with the calls. A new callback gets a `faulty_*` mock and an entry in the fault lists, and a new option that changes which callbacks the solver or stability check calls gets a case in their sweep. An error that only arises inside an internal callee's algorithm is not provoked through the caller, since the inputs that drive the callee there belong in the callee's own test.
|
|
133
|
+
|
|
134
|
+
**Helper procedures in test files.**
|
|
135
|
+
- A `check_*` (or other non-`bind(C)`) helper earns its existence by eliminating real duplication, never merely to keep one case's code out of the registered test's body — five near-identical one-case functions just relocate the duplication; write one function parameterized over the case, or a loop with a shared body.
|
|
136
|
+
- A helper that would be called once without arguments, only reading and writing its host's variables, is part of the host's body and stays inline. A single call justifies a separate procedure only for a mock callback, which has to be a procedure, or for a function of its arguments used in an expression, such as a `ref_*` reimplementation.
|
|
137
|
+
- A helper only one procedure uses is an internal procedure of that procedure, after its own `contains`, unless it is one of a family of `ref_*` twins whose other members live at module level. An internal procedure cannot contain another, so helpers calling each other are nested side by side in their common caller. Mocks in `opentrustregion_mock.f90`/`c_interface_mock.f90` stay module procedures however many callers they have, since those files exist to export them.
|
|
138
|
+
- Mock callbacks, which a test assigns to a procedure pointer, likewise stay module procedures however few callers they have: gfortran implements a pointer to an internal procedure with a trampoline, which on Linux needs an executable stack that glibc ≥ 2.41 refuses to `dlopen`, breaking the ctypes-driven testsuite (macOS uses heap trampolines, so it looks fine locally).
|
|
139
|
+
- An internal procedure uses its host's variables and imports directly instead of redeclaring them or taking them as arguments that every call fills with the host's same-named variable. What must stay its own (loop indices, which would otherwise overwrite a host loop calling it, and dummies receiving a slice or a different array) gets a name the host does not use, so that it never masks a host name.
|
|
140
|
+
- Helpers used by several procedures sit together near the top of the file, before the first `test_*` function, grouped by role so that each group only uses the ones above it: mocks and their support, then generators of random or structured data, then fixtures that set up objects or collect their state, then `ref_*` reimplementations, then `check_*` helpers. Where a group uses helpers of a group that this order puts after it, as the reference-settings fixtures in `test_reference.f90` use `ref_character_to_c`, the dependency order wins and the used group moves up. Within a group, helpers concerning the same object stay next to each other, in the order of the production code they serve (deterministic constructors ahead of random generators); `check_*` helpers follow the order of the tests they drive.
|
|
141
|
+
- Names follow the role: `generate_random_<what>` for a generator of random data, `<what>_matrix` for a deterministic constructor (`identity_matrix`), `setup_<what>` for a fixture that sets up an object.
|
|
142
|
+
|
|
143
|
+
**Verify a new test actually fails when the routine is broken.** Mutate the routine (flip a sign, swap an index, drop a term), rebuild, confirm the test fails, then restore. Mutate one routine per rebuild, or several at once only when no test runs more than one of them: the unit tests mock callbacks, not internal callees, so a mutated callee also fails its callers' tests and can hide that a caller's own mutation survived. Leave a second between rebuilds, since a rebuild started in the same second as the previous one can skip relinking `libotrtestsuite` and run the old code. Some mutations are legitimately benign for a given input (scaling safeguards, guards on paths the test doesn't reach) — pick a different mutation rather than concluding the test is weak.
|
|
144
|
+
|
|
145
|
+
**Before finishing:** build with `-DCMAKE_BUILD_TYPE=Debug` (`-Wall -Wextra -fcheck=all`) and run the suite — it catches out-of-bounds accesses release silently tolerates.
|
|
146
|
+
|
|
147
|
+
### CMake options that matter
|
|
148
|
+
|
|
149
|
+
- `INTEGER_SIZE` (`4` or `8`): library integer width. Unset → CMake autodetects, trying 32-bit BLAS/LAPACK first. Output library named `libopentrustregion_32.*` / `_64.*`. `USE_ILP64` (set whenever the BLAS/LAPACK integer size is 8, including autodetection's 64-bit fallback) switches the Fortran kinds `ip` and `c_ip` to 64-bit as a private definition of the library target, which exports `OTR_ILP64` instead to switch the header's `c_int`, so that C programs linking it see the same width without a generic macro leaking into their sources, and remaps BLAS/LAPACK symbols to `_64` variants when `check_fortran_function_exists` finds them.
|
|
150
|
+
- `BLAS_LIBRARIES` / `LAPACK_LIBRARIES`: overriding autodetection requires `INTEGER_SIZE` as well — providing the libraries without it is a fatal error. `OpenTrustRegion_HOST_PROVIDES_BLAS` (the host links BLAS/LAPACK) likewise requires `INTEGER_SIZE` and skips building the testsuite.
|
|
151
|
+
- `OpenTrustRegion_BUILD_TESTING` (default `ON` when top-level): builds `libotrtestsuite`, which Python loads to drive the Fortran tests. It carries the library's objects itself (`$<TARGET_OBJECTS:opentrustregion>`) rather than only linking a shared `libopentrustregion`: Flang gives every shared library its own copy of a derived type's descriptor, so a `c_callbacks_type` bundle a test constructs would fail the wrappers' `select type` in a separate library. The library itself only inspects bundles it built, so hosts are unaffected.
|
|
152
|
+
- `CONDA_BUILD=1` env var (set automatically by conda-build): selects an override that sets `wheel.cmake = false`, so scikit-build-core skips CMake entirely and emits a pure-Python wheel. The conda-forge recipe builds the Fortran library as its own `opentrustregion` package and `python_interface.py` loads it from the conda prefix at run time, so `pyopentrustregion` must not compile or ship a second copy.
|
|
153
|
+
- `OTR_RELEASE_WHEEL=1` env var: selects the release-wheel overrides in `pyproject.toml`, which pin `INTEGER_SIZE=4`, set `OpenTrustRegion_BUILD_TESTING=OFF`, exclude `testsuite.py`/`test_data` from the wheel and, on macOS, link Accelerate and a static Fortran runtime. Every pip build without it keeps the testsuite, so `python -m pyopentrustregion.testsuite` works from a source or editable install. An override replaces a table such as `cmake.define` unless it sets `inherit.cmake.define = "append"`, which the release overrides do, so they name only the defines they add.
|
|
154
|
+
|
|
155
|
+
### Preprocessing and integer kinds
|
|
156
|
+
|
|
157
|
+
All Fortran sources compile with `Fortran_PREPROCESS ON`. Integer kind selection and the BLAS/LAPACK 64-bit symbol remap (`ddot=ddot_64`, etc.) happen via `#ifdef USE_ILP64` and `add_compile_definitions` in CMake — never hardcode integer kinds.
|
|
158
|
+
|
|
159
|
+
BLAS/LAPACK are called through implicit interfaces (`external :: dgemm`), so gfortran infers each dummy argument's kind from the first call site and rejects a later call that disagrees. Both rules below are invisible in the default 32-bit build, where `ip` is `int32` and `1_ip == 1`:
|
|
160
|
+
|
|
161
|
+
- **Every integer argument to a BLAS/LAPACK routine must be `ip`-kinded** — `1_ip` not `1` for increments/leading dimensions, `size(x, kind=ip)` when passing on a `size()` result. Same for integers passed to project routines with an explicit `integer(ip)` dummy. Locals used as LAPACK arguments must be declared `integer(ip)`.
|
|
162
|
+
- **A newly used BLAS/LAPACK symbol must be added to the remap lists in `CMakeLists.txt`** (`add_compile_definitions("name=name_64")`, guarded by `BLAS_64`/`LAPACK_64`) — including test sources (`zheev` reaches the build only via `tests/opentrustregion_system_tests.f90`). A missing entry links the 32-bit-integer symbol from an ILP64 build, corrupting arguments at runtime rather than failing to build.
|
|
163
|
+
|
|
164
|
+
**Verifying an ILP64 build without an ILP64 BLAS.** Most dev machines only have 32-bit-integer BLAS, so `check_fortran_function_exists("sgemm_64")` fails and the remap is never exercised. Force it by pre-seeding the cache variables and checking the symbols the objects actually reference:
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
cc -shared -o /tmp/blas64stub.dylib /tmp/stubs.c # one empty function per name_64_ symbol
|
|
168
|
+
cmake -S . -B /tmp/build_ilp64 -DINTEGER_SIZE=8 \
|
|
169
|
+
-DBLAS_LIBRARIES=/tmp/blas64stub.dylib -DLAPACK_LIBRARIES=/tmp/blas64stub.dylib \
|
|
170
|
+
-DBLAS_64=1 -DLAPACK_64=1 -DBUILD_SHARED_LIBS=ON
|
|
171
|
+
cmake --build /tmp/build_ilp64
|
|
172
|
+
find /tmp/build_ilp64 -name '*.o' | xargs nm -u | awk '{print $NF}' | grep -E '^_?[dz][a-z0-9]+_$' # must print nothing
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The check prints every BLAS/LAPACK symbol referenced without the `_64` suffix. On macOS a clean link proves the same, but Linux links shared libraries with undefined symbols, so rely on the check there. Neither verifies numerical behaviour, which needs a real ILP64 BLAS.
|
|
176
|
+
|
|
177
|
+
### Known gotchas
|
|
178
|
+
|
|
179
|
+
- **A `size()`/literal-kind mistake in a BLAS call passes the default build and only breaks under `INTEGER_SIZE=8`**, as `Error: Type mismatch between actual argument at (1) and actual argument at (2) (INTEGER(4)/INTEGER(8))` pointing at two unrelated call sites of the same routine — the two that disagree, not the one that's wrong. Fix by making every integer argument `ip`-kinded, not by changing the named site.
|
|
180
|
+
- **`gfortran -fsyntax-only -I<moddir>` gives false confidence.** It checks only the pointed-to file against whatever `.mod` files already exist — it doesn't re-verify those `.mod`s. Editing a `type` whose fields are used across modules can leave a stale consumer `.mod` "passing" syntax-only checks while a real build breaks with `Fatal Error: Mismatch in components of derived type '...': expecting 'X', but got 'Y'`. Always confirm interface changes with `cmake --build`, not `-fsyntax-only`.
|
|
181
|
+
- **Intel stores `.true.` as -1 unless built with `-fpscomp logicals`.** C's `bool` only accepts 0 and 1, so every `logical(c_bool)` the library hands to C would hold an invalid value, which the C compiler may or may not happen to read as true. The Intel branch of `CMakeLists.txt` therefore adds the flag for every build type, and the C integration tests read the bytes of the bools the library writes (`is_c_bool`) rather than trusting a comparison.
|
|
182
|
+
- **gfortran 16 rejects defining anything through the associate name of an `intent(in)` pointer dummy**, which gfortran 15 and Flang accept: `select type (context)` followed by `context%n_calls = ...`, or `select type (callbacks => context)` followed by a pointer assignment like `callbacks%stability%hess_x => ...`, fails with `Dummy argument 'context' with INTENT(IN) in variable definition context` or `Associate-name 'callbacks' cannot appear in a variable definition context ... because its target ... cannot, either`. Point a local pointer at the selected context (`state => context`) and define through it. The macOS release wheels build with Homebrew's newest gcc, so they meet a new gfortran first.
|
|
183
|
+
- **Flang rejects code gfortran and Intel accept.** A component of derived type needs an explicit initializer (`= stability_settings_type()`) for the type's default structure constructor to be valid, even when every component of its own type has one, and an `X` edit descriptor needs its count (`1X`). Neither shows up without the Flang build.
|
|
184
|
+
- **`pip install` does not write into the source tree.** scikit-build-core configures CMake in a temporary directory, so a `build/` left in the repo is one you created yourself, which stays rebuildable with the system `cmake`, or one a pip build was told to keep with `-C build-dir=build`, as the coverage job does. The latter stays rebuildable only if scikit-build-core used a system CMake 3.24+ and Ninja or Make; otherwise it installed them from PyPI into pip's temporary build environment, whose paths the build directory's cache keeps after pip deletes it, so a later `cmake --build build` fails with `<temp-path>/cmake: No such file or directory`. It also means that a run from the checkout's root after `pip install .` imports the checkout's `pyopentrustregion` directory, which holds no library, and fails with `FileNotFoundError: Cannot find any of the libraries [...]` unless `<repo>/build` holds one, which it then loads instead of the installed library; run an installed build from another directory, as the CI jobs do (`working-directory: ${{ runner.temp }}`).
|
|
185
|
+
- **A local sdist packs untracked files.** scikit-build-core selects the files of the sdist by `.gitignore` and `sdist.exclude`, not by what git tracks, so an sdist built from a working tree also carries untracked, unignored files such as a `build_manual/`. Build a local sdist from a clean checkout or `git worktree`; the release sdists come from CI's fresh checkouts.
|
|
186
|
+
- **The Python driver only finds a development build in `<repo>/build`.** `python_interface.py` searches the conda prefix, then the installed package, then the `pyopentrustregion/` directory, then `<repo>/build` — a manually-configured `build_manual/` is invisible to it, and it silently loads whatever's in `build/` instead. To drive a custom build directory, run from a scratch directory with symlinks named `pyopentrustregion` and `build`:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
mkdir -p /tmp/run && cd /tmp/run
|
|
190
|
+
ln -sfn <repo>/pyopentrustregion pyopentrustregion
|
|
191
|
+
ln -sfn <repo>/build_manual build
|
|
192
|
+
python3 -m pyopentrustregion.testsuite
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Core library
|
|
196
|
+
|
|
197
|
+
### Source layout
|
|
198
|
+
|
|
199
|
+
- `src/opentrustregion.f90` — the numerical core, the module `opentrustregion_core`: `solver`, `stability_check`, the Davidson/Jacobi-Davidson/truncated-CG subsystem solvers, settings derived types, callback abstract interfaces, error-code constants, several thousand lines. The same file ends with the module `opentrustregion`, a facade that re-exports only the entities forming the Fortran interface, the counterpart of the C header and the Python package (the kinds `rp` and `ip`, `solver`, `stability_check`, the settings and state types, the callback interfaces, the error codes of the README table and the verbosity levels, but not the default settings, which equal the structure constructors without arguments, or `kw_len`, which only C and Python need to size their keyword buffers), so that a host which copies the single file or uses the module without `only:` sees nothing internal, while every other entity of the core can change in any version. Everything inside the library and the tests uses `opentrustregion_core`; only the system tests import the entry points and types they call through the facade, as a host does, so that a name missing from it fails to compile. The CMake target exports the directory of the modules, `<build>/modules` in the build tree and `include/opentrustregion_32` or `_64` once installed, since they differ between integer sizes, and the CI job of the CMake installation (`dev-build`) builds and runs the C and the Fortran program of the consumer project in `.github/consumer/` against the installed package.
|
|
200
|
+
- `src/c_interface.f90` — `bind(C)` wrapper module. Collects the C callback pointers in a per-call `c_callbacks_type` bundle held on the entry point's stack and passed through as the opaque context, and adapts C-style `(*)` arrays + return-code functions into Fortran-style `(:)` arrays + `intent(out) :: error` subroutines. Also holds the `bind(C)` twins of the states and `copy_state_to_c`, which copies the state into the host's record before every callback into C and when the call returns, and exports the sizes of the `bind(C)` settings and state types (`solver_settings_size`, `stability_settings_size`, `solver_state_size`, `stability_state_size`), which the C layout tests compare with the header's structs and `python_interface.py` with its ctypes structures. The module-scope procedure pointers `solver`/`stability_check` are test-injection seams, which only the wrappers' unit tests point at mocks, restoring them afterwards.
|
|
201
|
+
- `include/opentrustregion.h` — C header mirroring `solver_settings_type` / `stability_settings_type` and `solver_state_type` / `stability_state_type` as C structs, plus `*_init()` helpers and `solver`/`stability_check` prototypes, whose `const` settings pointers may be `NULL` for the defaults and whose state pointers may be `NULL` for none.
|
|
202
|
+
- `pyopentrustregion/python_interface.py` — ctypes wrapper. `SolverSettings`/`StabilitySettings` wrapper classes around the `SolverSettingsC`/`StabilitySettingsC` `ctypes.Structure` mirrors of the C structs, read-only `SolverState`/`StabilityState` classes around `SolverStateC`/`StabilityStateC`, Python callbacks wrapped with `CFUNCTYPE`, integer error codes converted to `RuntimeError`, with the exception of a failing callback function as its cause. `CallbackInterface.__call__` catches every exception raised in an interface, since ctypes drops an exception that leaves a callback function and hands the library an undefined return value. An interruption, such as a `KeyboardInterrupt`, stops the call and is raised as it is. Python raises a SIGINT that arrives while the library runs on entering the next callback function, before any `try`, so a call from the main thread (`call_library`) replaces Python's default handler by `defer_interruption` while it runs. That handler raises at once while a callback function is innermost and otherwise records the interruption in the per-thread `interruption_state`, where the next callback function with an error channel reports it as an error or, without one, the call raises it when it returns; the logging function, which has no error channel, records its interruptions there too. The marks of which of the nested calls is innermost are per thread, so solves still nest and run concurrently. Settings objects accept only their settings, their callback functions and the attributes they set themselves (`__setattr__`), expose no `initialized` flag, which Python settings always carry set, and get a C struct of their own when copied, as states do, so that changing a copy leaves the original unchanged. The arrays the interfaces hand a callback function as inputs are read-only. At import, `check_struct_sizes` compares the sizes of the ctypes structures with those the library exports and raises an `ImportError` when they differ, as for a library of another version, separately installed or a stale build, whose settings and states would otherwise be read and written with the wrong layout; a layout change that keeps the sizes goes undetected.
|
|
203
|
+
- `tests/` — `opentrustregion_unit_tests.f90` / `c_interface_unit_tests.f90` (Fortran/C interface unit tests, both in `libotrtestsuite`), `opentrustregion_system_tests.f90` (system tests against `pyopentrustregion/test_data/`), `c_integration_tests.c` (integration tests of the real library through the C header) with `c_integration_tests.h`, which declares the capture of the standard error and the warnings, `opentrustregion_mock.f90` (mocks of `solver`/`stability_check` for the C-interface unit tests), `c_interface_mock.f90` (`bind(C)` mocks for the Python interface unit tests), `test_reference.f90` (tolerances, reference values and shared fixtures). The C and Python integration tests read the Hartmann 6D problem from the `hartmann6d_*` `bind(C)` data that `opentrustregion_unit_tests.f90` exports.
|
|
204
|
+
|
|
205
|
+
### Fortran/C/Python interfaces must stay consistent
|
|
206
|
+
|
|
207
|
+
The same callback signatures, settings and state fields, and defaults are described in seven places that must agree — any change to a callback signature or a settings or state field (add/remove/reorder) must land in all seven in the same PR (a state field in all but item 5, since a state's defaults are the default initialization of its type), while a default value changes only in its definition (item 5) and has to stay distinct from its reference value (item 7), except for the logicals:
|
|
208
|
+
|
|
209
|
+
1. Fortran abstract interfaces, `solver_settings_type`/`stability_settings_type` and `solver_state_type`/`stability_state_type` in `src/opentrustregion.f90`, and for a new callback interface or error code its entry in the `only:` list of the facade module `opentrustregion` at the end of that file.
|
|
210
|
+
2. C abstract interfaces and `bind(C)` `solver_settings_type_c`/`stability_settings_type_c` in `src/c_interface.f90`, with the field-by-field conversions `assign_*_f_c`/`assign_*_c_f`, and for a callback its slot in `c_callbacks_type`, its argument of `store_optional_c_callbacks` and its `*_f_wrapper`; the state twins `solver_state_type_c`/`stability_state_type_c` with their conversions `assign_*_state_c_f`.
|
|
211
|
+
3. C struct layouts/typedefs in `include/opentrustregion.h`.
|
|
212
|
+
4. `SolverSettingsC`/`StabilitySettingsC` and `SolverStateC`/`StabilityStateC` ctypes `_fields_` and `CFUNCTYPE` declarations in `pyopentrustregion/python_interface.py`.
|
|
213
|
+
5. The default initializers of the components of `solver_settings_type`/`stability_settings_type` in `src/opentrustregion.f90`, the only definition of the default values apart from the automatic stability `n_random_trial_vectors` and the internal check's `stop_on_instability` (see **Settings types**), which C and Python obtain as described there.
|
|
214
|
+
6. Argument lists, snippets and the descriptions of the state fields in `README.md`.
|
|
215
|
+
7. The reference settings in `tests/test_reference.f90`: the value in `ref_solver_settings`/`ref_stability_settings`, the comparisons `equal_*` and `equal_*_c`, the field-by-field export `get_reference_*_values` and the lookup `reference_field`, plus its check in the C `test_settings_layout` (a `check_field`/`check_keyword` line, or for a logical its name and field in the two parallel arrays a static assert keeps in step), and for a callback its line in `callbacks_unset`, `unset_callbacks` and `callbacks_wrapped` and a distinct address in `get_reference_*_values`; for a state field its value in `ref_solver_state`/`ref_stability_state`, the comparison `equal_*_state_c`, the export `get_reference_*_state_values`, the lookup `reference_state_field` and its `check_state_field` line in the C `test_state_layout`, plus a value, distinct within its kind, in the mock states of `opentrustregion_mock.f90`. The Python settings and layout tests read the fields by name and need no change. A new or changed callback also reaches the tests outside these seven places: its checkers `check_<callback>_funptr` and `check_<callback>_c_funptr` in `test_reference.f90`, the mocks of both unit suites including a `faulty_*` one for the fault-injection sweeps, the callbacks of `c_integration_tests.c` and the callback names listed in the Python tests in `testsuite.py`.
|
|
216
|
+
|
|
217
|
+
Error-origin codes (`error_obj_func` etc.) must also stay synchronized with the README error-code table.
|
|
218
|
+
|
|
219
|
+
### Error codes
|
|
220
|
+
|
|
221
|
+
Encoded as `OOEE` integers (origin × 100 + specific code, see README). The Fortran core uses `add_error_origin` to tag a non-zero callback error with its origin (e.g. `error_update_orbs = 1200`). Every callback but the convergence check and the logging function, which return no values that can fail to be finite, is called through its `invoke_*` routine, which counts the call where the state counts it, tags the callback's error with its origin and rejects outputs that are not finite with an untagged error of its own, which the solver or the stability check then reports as theirs (`0101`/`0201`), except that an objective function value that is not a number counts as infinite, which rejects the step. No other routine calls a callback or checks its outputs for finiteness, except the step acceptance, which handles the infinite objective function values `invoke_obj_func` passes on and steps that overflow, and a new callback gets an `invoke_*` routine. C entry points return the integer directly; Python wrappers raise a `RuntimeError` that includes the raw code as-is — they don't decode it into origin/specific parts, so callers must read the README table themselves. New origins: add as a parameter in `opentrustregion.f90` and keep the README table in sync.
|