From 4f7057b5c230c5705e4e1a35f4d951194275b151 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 17:13:40 +0200 Subject: [PATCH 01/63] fix(core): stop ~ForeFireModel freeing an uninitialised pointer The constructor left `properties` uninitialised. Models that register at least one property overwrite it in registerProperty, but the six that register none never do -- Iso, heatFluxBasic, factorChemFlux, CraterVaporFluxModel, LavaSO2Flux and vaporFluxBasic. Destroying one of those passed whatever the member happened to be built over to delete[]. The same destructor also freed fuelPropertiesTable with delete[], while DataBroker::extractFuelProperties allocates it with a scalar new. Neither has been observed in a run, because FireDomain keeps its models in propModelsTable and fluxModelsTable and frees neither -- so nothing destroys a model today. Both become reachable as soon as anything does, including a test. --- src/ForeFireModel.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ForeFireModel.cpp b/src/ForeFireModel.cpp index 00748eb2..af10be58 100644 --- a/src/ForeFireModel.cpp +++ b/src/ForeFireModel.cpp @@ -17,11 +17,17 @@ ForeFireModel::ForeFireModel(const int & mindex, DataBroker* db) numProperties = 0; numFuelProperties = 0; fuelPropertiesTable = 0; + // Only the models that register at least one property allocate this, so + // without an explicit null the destructor of a model that registers none + // deletes whatever the member happened to be built over. Iso and + // heatFluxBasic are both in that group. + properties = 0; } ForeFireModel::~ForeFireModel() { if ( properties != 0 ) delete [] properties; - if ( fuelPropertiesTable != 0 ) delete [] fuelPropertiesTable; + // DataBroker::extractFuelProperties allocates this with a scalar new. + if ( fuelPropertiesTable != 0 ) delete fuelPropertiesTable; } void ForeFireModel::setDataBroker(DataBroker* db){ From 826e7b878add8a536acaa79b3f8f19b4ca055630 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 17:13:53 +0200 Subject: [PATCH 02/63] test(models): cover every propagation and flux model, build with -Wall -Wextra tests/runff runs one case through one propagation model (Rothermel, set in params.ff). Nothing was watching the other sixteen propagation models or any of the sixteen flux models. These tests exercise one model at a time, without a simulation. getSpeedForNode splits in two -- the DataBroker gathers properties into a double*, then the model does arithmetic on that array -- and only the second half is physics. ModelSandbox builds an empty FireDomain purely to instantiate models; Inputs fills their property array by property name rather than by index, so reordering a constructor's registerProperty calls cannot silently change what a test measures. Two kinds of assertion, and the README says which to trust. Invariants -- no spread without fuel, more wind never means less spread, a downslope is not an upslope, released energy is independent of how the time window is cut -- hold whatever the implementation is. Pinned rates of spread merely record what ForeFire produces today, at 1e-5 relative tolerance so that -march=native and FP contraction differences between runners do not trip them. Parameters are process-global, so the sandbox snapshots the parameter map and restores it on destruction; without that, three of five --order-by=rand seeds fail. Also compiles ForeFire's own sources with -Wall -Wextra. The warnings are not clean yet, so they are informational: FOREFIRE_WARNINGS_AS_ERRORS turns them fatal for whoever is clearing a file, and FOREFIRE_ENABLE_WARNINGS=OFF turns them off. NetCDF's headers are marked SYSTEM so their own warnings stay out of the output. The framework is doctest 2.5.3, vendored as a single header under third_party/ so a build needs no network. Nothing there is compiled into libforefireL. FOREFIRE_BUILD_TESTS defaults on, except for wheel builds. --- .github/workflows/macos.yml | 5 + .github/workflows/main.yml | 8 + CMakeLists.txt | 44 +- TESTING.md | 30 + tests/unit/CMakeLists.txt | 31 + tests/unit/README.md | 120 + tests/unit/main.cpp | 12 + tests/unit/model_sandbox.cpp | 189 + tests/unit/model_sandbox.h | 121 + tests/unit/test_flux_models.cpp | 199 + tests/unit/test_model_registry.cpp | 158 + tests/unit/test_propagation_models.cpp | 280 + third_party/README.md | 24 + third_party/doctest/doctest.h | 9148 ++++++++++++++++++++++++ 14 files changed, 10367 insertions(+), 2 deletions(-) create mode 100644 tests/unit/CMakeLists.txt create mode 100644 tests/unit/README.md create mode 100644 tests/unit/main.cpp create mode 100644 tests/unit/model_sandbox.cpp create mode 100644 tests/unit/model_sandbox.h create mode 100644 tests/unit/test_flux_models.cpp create mode 100644 tests/unit/test_model_registry.cpp create mode 100644 tests/unit/test_propagation_models.cpp create mode 100644 third_party/README.md create mode 100644 third_party/doctest/doctest.h diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 4f9bc61d..0a078f7a 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -26,6 +26,11 @@ jobs: - name: Check ForeFire version run: ./bin/forefire -v + # Also the check that the recorded rates of spread survive a different + # compiler and a different CPU. + - name: Run C++ unit tests + run: ctest --test-dir build --output-on-failure + - name: Install Python test dependencies run: pip3 install --break-system-packages lxml xarray netCDF4 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fa1e2725..86e6dcba 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,6 +43,14 @@ jobs: - name: Check ForeFire version run: ./bin/forefire -v + # The unit tests are built by install-forefire.sh along with everything + # else (FOREFIRE_BUILD_TESTS defaults on outside wheel builds), so this + # only has to run them. They cover the propagation and flux models one + # call at a time, which is what runff below cannot reach: it exercises a + # single case through Rothermel. + - name: Run C++ unit tests + run: ctest --test-dir build --output-on-failure + - name: Add Build/Runtime Diagnostics run: | echo "--- ForeFire Linkage ---" diff --git a/CMakeLists.txt b/CMakeLists.txt index ab9cc4cf..1c402cf7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ if(DEFINED SKBUILD) set(_ff_default_static ON) set(_ff_default_tools OFF) set(_ff_default_lfs OFF) + set(_ff_default_tests OFF) else() set(_ff_wheel_build OFF) set(_ff_default_mpi ON) @@ -39,6 +40,7 @@ else() set(_ff_default_static OFF) set(_ff_default_tools ON) set(_ff_default_lfs ON) + set(_ff_default_tests ON) endif() option(FOREFIRE_ENABLE_MPI "Enable MPI coupling when MPI is available" ${_ff_default_mpi}) @@ -47,6 +49,9 @@ option(FOREFIRE_BUILD_PYTHON "Build the pyforefire Python extension module" ${_f option(FOREFIRE_STATIC_CORE "Build the ForeFire core as a static library" ${_ff_default_static}) option(FOREFIRE_BUILD_TOOLS "Build the ANN_test helper executable" ${_ff_default_tools}) option(FOREFIRE_CHECK_LFS "Run the Git LFS data integrity check at configure time" ${_ff_default_lfs}) +option(FOREFIRE_BUILD_TESTS "Build the C++ unit tests, registered with CTest" ${_ff_default_tests}) +option(FOREFIRE_ENABLE_WARNINGS "Compile ForeFire's own sources with -Wall -Wextra" ON) +option(FOREFIRE_WARNINGS_AS_ERRORS "Fail the build on a compiler warning" OFF) # ---------------------------------- # Set C++ Standard @@ -154,7 +159,7 @@ if(DEFINED ENV{SRC_MESONH} AND DEFINED ENV{XYZ} AND DEFINED ENV{FF_STATIC}) message(FATAL_ERROR "Required static library not found: ${lib}") endif() endforeach() - target_include_directories(forefire_netcdf INTERFACE "${NETCDF_HOME}/include") + target_include_directories(forefire_netcdf SYSTEM INTERFACE "${NETCDF_HOME}/include") target_link_libraries(forefire_netcdf INTERFACE ${NETCDF_LIB} ${NETCDF_CXX_LIB} @@ -200,7 +205,9 @@ else() message(STATUS "NetCDF C: ${NETCDF_LIBRARY} (headers: ${NETCDF_INCLUDE_DIR})") message(STATUS "NetCDF C++: ${NETCDF_CXX_LIBRARY} (headers: ${NETCDF_CXX_INCLUDE_DIR})") - target_include_directories(forefire_netcdf INTERFACE ${NETCDF_CXX_INCLUDE_DIR} ${NETCDF_INCLUDE_DIR}) + # SYSTEM: the NetCDF C++4 headers trip -Wunused-variable, and that is not a + # warning anyone here can act on. + target_include_directories(forefire_netcdf SYSTEM INTERFACE ${NETCDF_CXX_INCLUDE_DIR} ${NETCDF_INCLUDE_DIR}) target_link_libraries(forefire_netcdf INTERFACE ${NETCDF_CXX_LIBRARY} ${NETCDF_LIBRARY}) endif() @@ -222,6 +229,25 @@ else() endif() endif() +# Warnings are a target property rather than a global flag on purpose: they +# should apply to code this project writes, not to pybind11's headers or to +# whatever a consumer compiles against the installed library. +set(_ff_warning_flags "") +if(FOREFIRE_ENABLE_WARNINGS) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") + list(APPEND _ff_warning_flags -Wall -Wextra) + elseif(MSVC) + list(APPEND _ff_warning_flags /W4) + endif() +endif() +if(FOREFIRE_WARNINGS_AS_ERRORS AND _ff_warning_flags) + if(MSVC) + list(APPEND _ff_warning_flags /WX) + else() + list(APPEND _ff_warning_flags -Werror) + endif() +endif() + # ---------------------------------- # Output Directories # ---------------------------------- @@ -262,6 +288,9 @@ target_include_directories(forefireL PUBLIC # its consumers the way the shared library does. find_package(Threads REQUIRED) target_link_libraries(forefireL PUBLIC forefire_netcdf Threads::Threads) +if(_ff_warning_flags) + target_compile_options(forefireL PRIVATE ${_ff_warning_flags}) +endif() # ---------------------------------- # Test Executable @@ -276,6 +305,17 @@ endif() # ---------------------------------- add_executable(forefire app/forefire/ForeFire.cpp app/forefire/AdvancedLineEditor.cpp) target_link_libraries(forefire PRIVATE forefireL) +if(_ff_warning_flags) + target_compile_options(forefire PRIVATE ${_ff_warning_flags}) +endif() + +# ---------------------------------- +# Unit Tests +# ---------------------------------- +if(FOREFIRE_BUILD_TESTS) + enable_testing() + add_subdirectory(tests/unit) +endif() # ---------------------------------- # Python Extension Module diff --git a/TESTING.md b/TESTING.md index dabd6859..f22ee1da 100644 --- a/TESTING.md +++ b/TESTING.md @@ -14,6 +14,22 @@ Install Python libraries via pip: pip3 install lxml xarray netCDF4 ``` +## Running the Unit Tests + +The C++ unit tests exercise propagation and flux models one call at a time, +without running a simulation. They are built alongside everything else and run +through CTest: + +```bash +cmake -S . -B build && cmake --build build -j +ctest --test-dir build --output-on-failure +``` + +They need no Python and no test data. Configure with +`-DFOREFIRE_BUILD_TESTS=OFF` to skip building them; wheel builds already do. + +`tests/unit/README.md` describes what they cover and how to add one. + ## Running the Core Test (`runff`) The primary automated test, validated in our CI pipeline, is located in `tests/runff/`. This test verifies core simulation, save/reload functionality, and NetCDF/KML output generation against reference files. @@ -36,6 +52,20 @@ The `ff-run.bash` script: The `tests/` directory contains other subdirectories (`mnh_*`, `python`, `runANN`) for potentially testing specific features like coupled simulations or Python bindings. A main `tests/run.bash` script exists but is not currently fully validated in CI. Refer to specific subdirectories for details if needed. +## Compiler Warnings + +ForeFire's own sources compile with `-Wall -Wextra` by default. The warnings +are not yet clean, so they are informational rather than fatal; two options +control this: + +* `-DFOREFIRE_ENABLE_WARNINGS=OFF` — build quietly. +* `-DFOREFIRE_WARNINGS_AS_ERRORS=ON` — fail the build on any warning. Useful + on a subset of files while clearing them; not yet usable repository-wide. + +The flags apply to `libforefireL`, the `forefire` executable and the unit +tests. NetCDF's headers are included as system headers so their warnings do +not appear. + ## Contributing Please see `CONTRIBUTING.md` for guidelines on contributing to ForeFire, including adding new tests. Report any issues via the repository's issue tracker. \ No newline at end of file diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 00000000..96136ce0 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,31 @@ +# ---------------------------------- +# C++ unit tests +# ---------------------------------- +# These link the core library and exercise single models in isolation, which +# is the half of the code tests/runff cannot reach: it runs one case through +# one propagation model, so a regression in any of the other models is +# invisible to it. + +add_executable(forefire_unit_tests + main.cpp + model_sandbox.cpp + test_flux_models.cpp + test_model_registry.cpp + test_propagation_models.cpp) + +target_link_libraries(forefire_unit_tests PRIVATE forefireL) +target_include_directories(forefire_unit_tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${PROJECT_SOURCE_DIR}/third_party") + +if(_ff_warning_flags) + target_compile_options(forefire_unit_tests PRIVATE ${_ff_warning_flags}) +endif() + +# doctest groups its cases into suites; registering one CTest entry per suite +# keeps `ctest` output useful without needing doctest's CMake integration. +foreach(_suite "model registry" "propagation models" "flux models") + string(REPLACE " " "_" _suite_id "${_suite}") + add_test(NAME "unit.${_suite_id}" + COMMAND forefire_unit_tests --test-suite=${_suite} --no-skipped-summary) +endforeach() diff --git a/tests/unit/README.md b/tests/unit/README.md new file mode 100644 index 00000000..c079bab2 --- /dev/null +++ b/tests/unit/README.md @@ -0,0 +1,120 @@ +# Unit tests + +C++ tests that exercise one model at a time, without running a simulation. + +`tests/runff` runs a whole case and diffs the output against reference files. +That catches physics drift along the path it takes — but it takes one path, +through one propagation model (`Rothermel`, set in `params.ff`). Nothing was +watching the other eighteen propagation models or any of the sixteen flux +models. These tests are for them. + +## Running them + +They build with everything else, and are registered with CTest: + +```bash +cmake -S . -B build && cmake --build build -j +ctest --test-dir build --output-on-failure +``` + +Or run the binary directly, which gives finer control: + +```bash +./bin/forefire_unit_tests # everything +./bin/forefire_unit_tests --test-suite="flux models" +./bin/forefire_unit_tests --test-case="*Rothermel*" +./bin/forefire_unit_tests --list-test-cases +``` + +`-DFOREFIRE_BUILD_TESTS=OFF` skips building them. Wheel builds default to off. + +The framework is [doctest](https://github.com/doctest/doctest) 2.5.3, vendored +as a single header in `third_party/doctest/`. See that directory's README. + +## How a model gets tested + +`getSpeedForNode` splits in two: the DataBroker gathers properties out of the +simulation into a `double*`, and then the model does arithmetic on that array. +Only the second half is physics, and it needs nothing but the array — so +`ModelSandbox` builds an empty `FireDomain` purely to instantiate models, and +`Inputs` fills their property array by property *name* rather than by index. + +Addressing by name matters. A model's properties are numbered in the order its +constructor calls `registerProperty`, so a test written against raw indices +would keep passing after someone reorders that constructor, while silently +testing a different quantity. + +Parameters are process-global — every `SimulationParameters` method reads and +writes `GetInstance()` whatever instance it is called on — so a test setting +`Iso.speed`, or `burningDuration` to zero, would otherwise change the result of +whichever test ran next. `ModelSandbox` snapshots the parameter map on +construction and puts it back on destruction. + +That restore is load-bearing rather than precautionary: with it removed, three +of five `--order-by=rand` seeds fail. Running the suite under a few seeds is a +cheap way to check it still holds: + +```bash +./bin/forefire_unit_tests --order-by=rand --rand-seed=1337 +``` + +## What the assertions mean + +Two kinds, and they are not equally trustworthy. + +**Invariants** — no spread without fuel, more wind never means less spread, +a downslope is not an upslope, total released energy does not depend on how +the time window is cut. These should hold whatever the implementation is, and +they are the ones worth trusting. + +**Pinned values** — `recorded rates of spread for the standard fuel` is a +record of what ForeFire produces today, at a 1e-5 relative tolerance so that +`-march=native` and floating-point contraction differences between CI runners +do not trip it. A pin moving means the model changed; that may well be +intended, but it should be a decision rather than a surprise. The pins carry +no claim of matching published values. + +## Things found while writing these, and not fixed here + +Two of them are why `test_model_registry.cpp` only destroys the models that +register no properties. + +**Models are never destroyed in a normal run.** `FireDomain` keeps them in +`propModelsTable` and `fluxModelsTable` and frees neither, so every model a +simulation instantiates is leaked. That is why the two problems below have +never been observed: the code that would trip them does not run. + +**The `properties` array is deleted twice.** Seventeen flux models and two +propagation models delete `properties` in their own destructor, and +`~ForeFireModel` deletes it again. Most of them also use scalar `delete` on an +array allocated with `new[]`. So destroying any model that registers at least +one property is a double free. Fixing it means removing the `delete` from each +derived destructor and leaving it to the base class — nineteen files, worth +doing as its own change. + +`~ForeFireModel` itself was fixed while writing these tests: it left +`properties` uninitialised, so destroying a model that registers *no* +properties — `Iso`, `heatFluxBasic` — deleted whatever the member happened to +be built over. It also deleted `fuelPropertiesTable`, allocated with a scalar +`new`, with `delete[]`. + +**`BalbiNov2011` responds non-physically to live fuel moisture at the values +in the shipped fuel table.** `xsi` exceeds 1 for fuel 1 of +`tests/runff/fuels.csv`, the flame temperature term goes negative, and `R00` +raises it to the fourth power. Rate of spread therefore falls with rising live +moisture up to about `Ml = 0.8` and then climbs again: 1.3e-3 m/s at +`Ml = 0.5`, 1.9e-8 at `Ml = 0.8`, 8.2e-5 at `Ml = 1.0` — which is the value the +table ships. `drier live fuel spreads faster, over the physical range` stops +short of that inversion rather than asserting it is correct. + +## Adding a test + +Models needing an external resource cannot be covered here: `ANNPropagationModel` +and `BMapLoggerForANNTraining` read a `.ffann` network in their constructor and +abort when it is missing. `tests/runANN` covers those. + +For anything else, add its name to the list in `test_model_registry.cpp`. If +`fillStandardConditions` knows every property it reads, it can also join +`standardFuelModels()` in `test_propagation_models.cpp` and inherit the +sweeps; otherwise teach `standardValues()` in `model_sandbox.cpp` the missing +properties first. diff --git a/tests/unit/main.cpp b/tests/unit/main.cpp new file mode 100644 index 00000000..7178467d --- /dev/null +++ b/tests/unit/main.cpp @@ -0,0 +1,12 @@ +/** + * @file main.cpp + * @brief Entry point for the ForeFire unit tests. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + * + * This translation unit exists only to generate doctest's main(). Keeping it + * on its own means the header is expanded once instead of in every test file. + */ + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" diff --git a/tests/unit/model_sandbox.cpp b/tests/unit/model_sandbox.cpp new file mode 100644 index 00000000..05e47efe --- /dev/null +++ b/tests/unit/model_sandbox.cpp @@ -0,0 +1,189 @@ +/** + * @file model_sandbox.cpp + * @brief Implementation of the unit-test scaffolding. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + */ + +#include "model_sandbox.h" + +#include "FireDomain.h" +#include "FluxModel.h" +#include "ForeFireModel.h" +#include "PropagationModel.h" +#include "SimulationParameters.h" + +#include "doctest/doctest.h" + +#include + +using libforefire::FireDomain; +using libforefire::FluxModel; +using libforefire::ForeFireModel; +using libforefire::PropagationModel; +using libforefire::SimulationParameters; + +namespace ff_test { + +const std::string& ModelSandbox::standardFuelTable() { + // Fuels 0 and 1 of tests/runff/fuels.csv. Fuel 0 has a zero fuel depth, + // which several models treat as "nothing to burn"; fuel 1 is the one the + // numerical tests use. + static const std::string table = + "Index;Rhod;Rhol;Md;Ml;sd;sl;e;Sigmad;Sigmal;stoch;RhoA;Ta;Tau0;" + "Deltah;DeltaH;Cp;Cpa;Ti;X0;r00;Blai;me\n" + "0;563.0;522.0;0.1;1.0;6099.0;7273.0;0;0.764;0.352;8.3;1.0;300;70000;" + "18169000.0;18167000.0;1800;1000;600;0.3;2.5e-05;4.0;0.3\n" + "1;563.0;522.0;0.1;1.0;6099.0;7273.0;0.24;0.764;0.352;8.3;1.0;300;70000;" + "18169000.0;18167000.0;1800;1000;600;0.3;2.5e-05;4.0;0.3\n"; + return table; +} + +namespace { + +/** The value SimulationParameters reports for a key it does not hold. + * + * The sentinel itself is a private static, so it is read back out of the class + * rather than duplicated here: any key that cannot plausibly have been set + * returns it. Assigning it to a key makes isValued() false again, which is the + * closest thing to erasing one that the interface offers. */ +const std::string& unsetValue() { + static const std::string sentinel = + SimulationParameters::GetInstance()->getParameter( + "ff_test.no.such.parameter"); + return sentinel; +} + +} /* anonymous namespace */ + +ModelSandbox::ModelSandbox(const std::string& fuelTable) + : params(SimulationParameters::GetInstance()) + , domain(0) + , nextPropagationIndex(0) + , nextFluxIndex(0) { + // Parameters are process-global on this branch: SimulationParameters' + // methods all operate on GetInstance() whatever instance they are called + // on, so a private set would be ignored. Snapshot instead, and put it back + // in the destructor, so that a test setting burningDuration to zero cannot + // reach the test that runs after it. + const std::vector keys = params->getAllKeys(); + for (size_t i = 0; i < keys.size(); i++) { + savedParameters[keys[i]] = params->getParameter(keys[i]); + } + + // The DataBroker parses the fuel table while the domain is being built, so + // this has to be in place before the domain exists. + params->setParameter("fuelsTable", fuelTable); + + libforefire::FFPoint sw(0., 0., 0.); + libforefire::FFPoint ne(1000., 1000., 0.); + domain = new FireDomain(0., sw, ne); +} + +ModelSandbox::~ModelSandbox() { + // The domain owns its broker, which owns the models it handed out. + delete domain; + + // Restore what was there, and blank whatever this sandbox introduced. + const std::vector keys = params->getAllKeys(); + for (size_t i = 0; i < keys.size(); i++) { + std::map::const_iterator saved + = savedParameters.find(keys[i]); + params->setParameter(keys[i], + saved != savedParameters.end() ? saved->second + : unsetValue()); + } +} + +PropagationModel* ModelSandbox::propagation(const std::string& name) { + return domain->propModelInstanciation(nextPropagationIndex++, name); +} + +FluxModel* ModelSandbox::flux(const std::string& name) { + return domain->fluxModelInstanciation(nextFluxIndex++, name); +} + +Inputs::Inputs(ForeFireModel* m) + : model(m) + , values(m != 0 ? m->numProperties : 0, 0.) { +} + +size_t Inputs::indexOf(const std::string& property) const { + for (size_t i = 0; i < model->wantedProperties.size(); i++) { + if (model->wantedProperties[i] == property) return i; + } + return model->wantedProperties.size(); +} + +bool Inputs::has(const std::string& property) const { + return model != 0 && indexOf(property) < model->wantedProperties.size(); +} + +Inputs& Inputs::set(const std::string& property, double value) { + const size_t i = indexOf(property); + REQUIRE_MESSAGE(i < values.size(), + "model " << model->getName() << " does not read '" + << property << "'"); + values[i] = value; + return *this; +} + +double* Inputs::data() { + return values.empty() ? 0 : &values[0]; +} + +namespace { + +/** Fuel 1 of the standard table, plus the ambient quantities a model may ask + * for on top of the fuel. Anything not listed here makes fillStandardConditions + * give up rather than leave a property at zero. */ +const std::map& standardValues() { + static std::map v; + if (!v.empty()) return v; + v["fuel.Rhod"] = 563.0; + v["fuel.Rhol"] = 522.0; + v["fuel.Md"] = 0.1; + v["fuel.Ml"] = 1.0; + v["fuel.sd"] = 6099.0; + v["fuel.sl"] = 7273.0; + v["fuel.e"] = 0.24; + v["fuel.Sigmad"] = 0.764; + v["fuel.Sigmal"] = 0.352; + v["fuel.stoch"] = 8.3; + v["fuel.RhoA"] = 1.0; + v["fuel.Ta"] = 300.0; + v["fuel.Tau0"] = 70000.0; + v["fuel.Deltah"] = 18169000.0; + v["fuel.DeltaH"] = 18167000.0; + v["fuel.Cp"] = 1800.0; + v["fuel.Cpa"] = 1000.0; + v["fuel.Ti"] = 600.0; + v["fuel.X0"] = 0.3; + v["fuel.r00"] = 2.5e-05; + v["fuel.Blai"] = 4.0; + v["fuel.me"] = 0.3; + // Ambient conditions and front geometry, not fuel properties. + v["slope"] = 0.0; + v["normalWind"] = 0.0; + v["moisture"] = 0.1; + v["deadMoisture"] = 0.1; + v["temperature"] = 300.0; + v["frontDepth"] = 1.0; + v["frontCurvature"] = 0.0; + return v; +} + +} /* anonymous namespace */ + +bool fillStandardConditions(Inputs& inputs, ForeFireModel* model) { + const std::map& known = standardValues(); + for (size_t i = 0; i < model->wantedProperties.size(); i++) { + const std::string& property = model->wantedProperties[i]; + std::map::const_iterator it = known.find(property); + if (it == known.end()) return false; + inputs.set(property, it->second); + } + return true; +} + +} /* namespace ff_test */ diff --git a/tests/unit/model_sandbox.h b/tests/unit/model_sandbox.h new file mode 100644 index 00000000..c1a78636 --- /dev/null +++ b/tests/unit/model_sandbox.h @@ -0,0 +1,121 @@ +/** + * @file model_sandbox.h + * @brief Minimal scaffolding to exercise a single model outside a simulation. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + */ + +#ifndef FF_TESTS_MODEL_SANDBOX_H +#define FF_TESTS_MODEL_SANDBOX_H + +#include +#include +#include + +namespace libforefire { +class FireDomain; +class FluxModel; +class ForeFireModel; +class PropagationModel; +class SimulationParameters; +} + +namespace ff_test { + +/** A fire domain that exists only to hand out models. + * + * Propagation and flux models are not constructible on their own: their + * constructors call back into the DataBroker, which calls back into the + * FireDomain. So a unit test still needs a domain — but only an empty one, + * with no landscape, no fuel map and no fire. Everything a model reads from + * the simulation arrives through the `double*` it is handed, which is what + * makes `getSpeed` and `getValue` testable in isolation. + * + * Parameters are process-global: every SimulationParameters method reads and + * writes SimulationParameters::GetInstance() whatever instance it is called + * on, so there is no per-domain parameter set to hand out. A test that sets + * `Iso.speed` or `burningDuration` would therefore change the result of the + * next one. To stop that, the sandbox snapshots the whole parameter map on + * construction and restores it on destruction — see ~ModelSandbox. + */ +class ModelSandbox { +public: + /** Builds the domain. Parameters set after this point are still seen by + * models, because models read them in their constructor — but the fuel + * table is read here, so it has to be passed in. */ + explicit ModelSandbox(const std::string& fuelTable = standardFuelTable()); + + ~ModelSandbox(); + + /** The global parameter set, restored to its prior contents when this + * sandbox dies. Set values here *before* asking for a model: that is when + * the model reads its coefficients. */ + libforefire::SimulationParameters* parameters() const { return params; } + + /** Instantiates a registered propagation model, or returns 0 if the name + * is not in the registry. Each call gets a fresh model index. */ + libforefire::PropagationModel* propagation(const std::string& name); + + /** Instantiates a registered flux model, or returns 0 if unknown. */ + libforefire::FluxModel* flux(const std::string& name); + + /** The fuel table used by default: the columns of the Balbi/Rothermel + * family, as in tests/runff/fuels.csv. Models asking for fuel properties + * outside this set warn on construction; that is not a failure, it just + * means the table does not describe them. */ + static const std::string& standardFuelTable(); + +private: + ModelSandbox(const ModelSandbox&); + ModelSandbox& operator=(const ModelSandbox&); + + libforefire::SimulationParameters* params; + libforefire::FireDomain* domain; + std::map savedParameters; + int nextPropagationIndex; + int nextFluxIndex; +}; + +/** The property array a model reads, addressed by name instead of by index. + * + * A model's properties are numbered in the order its constructor calls + * registerProperty, so `valueOf[7]` means whatever the eighth call happened to + * register. Tests that hardcode those numbers break the moment someone + * reorders the constructor — silently, by testing a different quantity. This + * looks the index up from the model's own wantedProperties instead. + */ +class Inputs { +public: + explicit Inputs(libforefire::ForeFireModel*); + + /** Sets one property. Aborts the test if the model never asked for it, + * which is the honest outcome: the test is describing a model that does + * not exist. */ + Inputs& set(const std::string& property, double value); + + /** True if the model registered this property. */ + bool has(const std::string& property) const; + + /** The array to hand to getSpeed/getValue. Null for a model with no + * properties, which is what those models are handed in production. */ + double* data(); + +private: + size_t indexOf(const std::string& property) const; + + libforefire::ForeFireModel* model; + std::vector values; +}; + +/** Fills every property a model asks for: fuel 1 of the standard table, still + * air on flat ground, and a front one metre deep with no curvature. + * Individual tests then override whatever they are actually varying. + * + * Returns false if the model reads something this helper has no value for, + * so a test can leave that model alone rather than run it on zeroes. + */ +bool fillStandardConditions(Inputs&, libforefire::ForeFireModel*); + +} /* namespace ff_test */ + +#endif /* FF_TESTS_MODEL_SANDBOX_H */ diff --git a/tests/unit/test_flux_models.cpp b/tests/unit/test_flux_models.cpp new file mode 100644 index 00000000..5d912ff1 --- /dev/null +++ b/tests/unit/test_flux_models.cpp @@ -0,0 +1,199 @@ +/** + * @file test_flux_models.cpp + * @brief Flux models, and the energy they are supposed to conserve. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + * + * A flux model answers "what was the mean flux over [bt, et] for a cell that + * ignited at at?". The atmospheric coupling calls it once per timestep with + * whatever window the atmospheric model happens to be using, and adds the + * result onto the surface fluxes. So the quantity that has to be right is not + * any single call but the integral: a cell must release the same total energy + * whether Meso-NH asks for it in one step or in fifty. + * + * tools/TODO.md lists exactly this as a wanted test — "mass conservation for + * heat fluxes". + */ + +#include "doctest/doctest.h" + +#include "model_sandbox.h" + +#include "FluxModel.h" +#include "SimulationParameters.h" + +#include +#include +#include + +using libforefire::FluxModel; +using ff_test::Inputs; +using ff_test::ModelSandbox; + +namespace { + +const double BURNING_DURATION = 300.0; +const double NOMINAL_FLUX = 1000000.0; +const double ARRIVAL_TIME = 1000.0; + +/** The two flux models with the same "constant nominal flux for a fixed + * duration" shape, and the parameter each one reads for its magnitude. */ +struct BasicFluxModel { + const char* name; + const char* magnitudeParameter; +}; + +const BasicFluxModel BASIC_MODELS[] = { + {"heatFluxBasic", "nominalHeatFlux"}, + {"vaporFluxBasic", "nominalVaporFlux"}, +}; + +const size_t BASIC_MODEL_COUNT = sizeof(BASIC_MODELS) / sizeof(BASIC_MODELS[0]); + +/** A sandbox with the burn duration and flux magnitude pinned, so the tests + * do not depend on the defaults, which differ between the two models. */ +void configure(ModelSandbox& sandbox, const BasicFluxModel& model) { + sandbox.parameters()->setDouble("burningDuration", BURNING_DURATION); + sandbox.parameters()->setDouble(model.magnitudeParameter, NOMINAL_FLUX); +} + +} /* anonymous namespace */ + +TEST_SUITE("flux models") { + +TEST_CASE("total released energy does not depend on how the window is cut") { + // The window runs from well before ignition to well after burnout, so + // every partition of it must integrate to the same total: the full burn. + const double windowStart = ARRIVAL_TIME - 137.0; + const double windowEnd = ARRIVAL_TIME + BURNING_DURATION + 211.0; + const double expectedTotal = NOMINAL_FLUX * BURNING_DURATION; + + for (size_t m = 0; m < BASIC_MODEL_COUNT; m++) { + ModelSandbox sandbox; + configure(sandbox, BASIC_MODELS[m]); + FluxModel* model = sandbox.flux(BASIC_MODELS[m].name); + REQUIRE(model != 0); + Inputs inputs(model); + + for (int steps = 1; steps <= 64; steps++) { + CAPTURE(BASIC_MODELS[m].name); + CAPTURE(steps); + + const double dt = (windowEnd - windowStart) / steps; + double total = 0.; + for (int i = 0; i < steps; i++) { + const double bt = windowStart + i * dt; + const double et = bt + dt; + total += model->getValue(inputs.data(), bt, et, ARRIVAL_TIME) * dt; + } + CHECK(total == doctest::Approx(expectedTotal).epsilon(1e-9)); + } + } +} + +TEST_CASE("energy is conserved across unevenly sized steps too") { + // An atmospheric model does not have to use a constant timestep, and the + // partial-overlap branches are where an off-by-one in the interval + // arithmetic would hide. + const double cuts[] = { + ARRIVAL_TIME - 500.0, ARRIVAL_TIME - 1.0, ARRIVAL_TIME, + ARRIVAL_TIME + 0.5, ARRIVAL_TIME + 17.0, ARRIVAL_TIME + 299.5, + ARRIVAL_TIME + BURNING_DURATION, ARRIVAL_TIME + BURNING_DURATION + 0.25, + ARRIVAL_TIME + BURNING_DURATION + 900.0, + }; + const size_t cutCount = sizeof(cuts) / sizeof(cuts[0]); + + for (size_t m = 0; m < BASIC_MODEL_COUNT; m++) { + CAPTURE(BASIC_MODELS[m].name); + ModelSandbox sandbox; + configure(sandbox, BASIC_MODELS[m]); + FluxModel* model = sandbox.flux(BASIC_MODELS[m].name); + REQUIRE(model != 0); + Inputs inputs(model); + + double total = 0.; + for (size_t i = 0; i + 1 < cutCount; i++) { + const double bt = cuts[i]; + const double et = cuts[i + 1]; + total += model->getValue(inputs.data(), bt, et, ARRIVAL_TIME) * (et - bt); + } + CHECK(total == doctest::Approx(NOMINAL_FLUX * BURNING_DURATION).epsilon(1e-9)); + } +} + +TEST_CASE("nothing is released outside the burning interval") { + for (size_t m = 0; m < BASIC_MODEL_COUNT; m++) { + CAPTURE(BASIC_MODELS[m].name); + ModelSandbox sandbox; + configure(sandbox, BASIC_MODELS[m]); + FluxModel* model = sandbox.flux(BASIC_MODELS[m].name); + REQUIRE(model != 0); + Inputs inputs(model); + + // Entirely before ignition. + CHECK(model->getValue(inputs.data(), ARRIVAL_TIME - 100.0, + ARRIVAL_TIME - 10.0, ARRIVAL_TIME) == doctest::Approx(0.0)); + // Entirely after burnout. + CHECK(model->getValue(inputs.data(), ARRIVAL_TIME + BURNING_DURATION + 10.0, + ARRIVAL_TIME + BURNING_DURATION + 100.0, + ARRIVAL_TIME) == doctest::Approx(0.0)); + } +} + +TEST_CASE("a window inside the burn reports the nominal flux") { + for (size_t m = 0; m < BASIC_MODEL_COUNT; m++) { + CAPTURE(BASIC_MODELS[m].name); + ModelSandbox sandbox; + configure(sandbox, BASIC_MODELS[m]); + FluxModel* model = sandbox.flux(BASIC_MODELS[m].name); + REQUIRE(model != 0); + Inputs inputs(model); + + CHECK(model->getValue(inputs.data(), ARRIVAL_TIME + 10.0, ARRIVAL_TIME + 20.0, + ARRIVAL_TIME) == doctest::Approx(NOMINAL_FLUX)); + } +} + +TEST_CASE("the instantaneous flux switches on at ignition and off at burnout") { + // bt == et is the separate branch every one of these models carries. + for (size_t m = 0; m < BASIC_MODEL_COUNT; m++) { + CAPTURE(BASIC_MODELS[m].name); + ModelSandbox sandbox; + configure(sandbox, BASIC_MODELS[m]); + FluxModel* model = sandbox.flux(BASIC_MODELS[m].name); + REQUIRE(model != 0); + Inputs inputs(model); + + const double before = ARRIVAL_TIME - 0.001; + const double during = ARRIVAL_TIME + BURNING_DURATION / 2.0; + const double after = ARRIVAL_TIME + BURNING_DURATION; + + CHECK(model->getValue(inputs.data(), before, before, ARRIVAL_TIME) + == doctest::Approx(0.0)); + CHECK(model->getValue(inputs.data(), ARRIVAL_TIME, ARRIVAL_TIME, ARRIVAL_TIME) + == doctest::Approx(NOMINAL_FLUX)); + CHECK(model->getValue(inputs.data(), during, during, ARRIVAL_TIME) + == doctest::Approx(NOMINAL_FLUX)); + CHECK(model->getValue(inputs.data(), after, after, ARRIVAL_TIME) + == doctest::Approx(0.0)); + } +} + +TEST_CASE("a zero-length burn releases nothing") { + for (size_t m = 0; m < BASIC_MODEL_COUNT; m++) { + CAPTURE(BASIC_MODELS[m].name); + ModelSandbox sandbox; + sandbox.parameters()->setDouble("burningDuration", 0.0); + sandbox.parameters()->setDouble(BASIC_MODELS[m].magnitudeParameter, NOMINAL_FLUX); + FluxModel* model = sandbox.flux(BASIC_MODELS[m].name); + REQUIRE(model != 0); + Inputs inputs(model); + + const double value = model->getValue(inputs.data(), ARRIVAL_TIME - 10.0, + ARRIVAL_TIME + 10.0, ARRIVAL_TIME); + CHECK(std::isfinite(value)); + CHECK(value == doctest::Approx(0.0)); + } +} + +} /* TEST_SUITE */ diff --git a/tests/unit/test_model_registry.cpp b/tests/unit/test_model_registry.cpp new file mode 100644 index 00000000..351b4b18 --- /dev/null +++ b/tests/unit/test_model_registry.cpp @@ -0,0 +1,158 @@ +/** + * @file test_model_registry.cpp + * @brief The model registry contains what it is supposed to contain. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + * + * Every propagation and flux model registers itself from a static initialiser + * that nothing references by name. That makes the whole set vulnerable to a + * build change: link the core out of a static archive rather than an object + * library and the linker drops the objects, the registry comes up empty, and + * the failure only shows up much later as "model not recognized" — or as a + * segfault. CMakeLists.txt has a comment about this and test_wheel.py checks + * for it on the Python side; these are the same checks for the C++ build. + */ + +#include "doctest/doctest.h" + +#include "model_sandbox.h" + +#include "FluxModel.h" +#include "PropagationModel.h" + +#include +#include + +using libforefire::FluxModel; +using libforefire::PropagationModel; +using ff_test::ModelSandbox; + +namespace { + +/** Every propagation model that builds without an external resource. + * + * ANNPropagationModel and BMapLoggerForANNTraining are deliberately absent: + * both read a .ffann network in their constructor and abort the process when + * it is missing, so they cannot be covered here. tests/runANN exercises them. + */ +const std::vector& propagationModels() { + static std::vector names; + if (!names.empty()) return names; + names.push_back("Balbi2015"); + names.push_back("Balbi2020"); + names.push_back("BalbiNov2011"); + names.push_back("BalbiNov2011Curv"); + names.push_back("BalbiNov2011TMdMl"); + names.push_back("BalbiUnsteady"); + names.push_back("CurvatureDriven"); + names.push_back("Farsite"); + names.push_back("FrontDepthDriven"); + names.push_back("Iso"); + names.push_back("IsotropicFuel"); + names.push_back("LavaPropagationModel"); + names.push_back("Rothermel"); + names.push_back("RothermelAndrews2018"); + names.push_back("SamplePropagationModel"); + names.push_back("TroisPourcent"); + names.push_back("WindDriven"); + return names; +} + +const std::vector& fluxModels() { + static std::vector names; + if (!names.empty()) return names; + names.push_back("BurnUpHeatFlux"); + names.push_back("CraterHeatFluxModel"); + names.push_back("CraterVaporFluxModel"); + names.push_back("factorChemFlux"); + names.push_back("ForeFireV1HeatFlux"); + names.push_back("ForeFireV1VaporFlux"); + names.push_back("heatFluxBasic"); + names.push_back("heatFluxFromObs"); + names.push_back("heatFluxNominal"); + names.push_back("LavaSO2Flux"); + names.push_back("SFMod"); + names.push_back("SFObs"); + names.push_back("SpottingFluxBasic"); + names.push_back("vaporFluxBasic"); + names.push_back("vaporFluxFromObs"); + names.push_back("vaporFluxNominal"); + return names; +} + +} /* anonymous namespace */ + +TEST_SUITE("model registry") { + +TEST_CASE("every propagation model is registered and names itself") { + ModelSandbox sandbox; + const std::vector& names = propagationModels(); + for (size_t i = 0; i < names.size(); i++) { + CAPTURE(names[i]); + PropagationModel* model = sandbox.propagation(names[i]); + REQUIRE(model != 0); + // A model whose getName() disagrees with the key it registered under + // cannot be found again by the name it reports. + CHECK(model->getName() == names[i]); + } +} + +TEST_CASE("every flux model is registered and names itself") { + ModelSandbox sandbox; + const std::vector& names = fluxModels(); + for (size_t i = 0; i < names.size(); i++) { + CAPTURE(names[i]); + FluxModel* model = sandbox.flux(names[i]); + REQUIRE(model != 0); + CHECK(model->getName() == names[i]); + } +} + +TEST_CASE("an unknown model name is refused rather than fatal") { + ModelSandbox sandbox; + PropagationModel* propagation = sandbox.propagation("NoSuchPropagationModel"); + FluxModel* flux = sandbox.flux("NoSuchFluxModel"); + CHECK(propagation == 0); + CHECK(flux == 0); +} + +TEST_CASE("a model with no properties can be destroyed") { + // Models that register no property never allocate their `properties` + // array, so they are the ones that expose whatever the base class leaves + // uninitialised: ~ForeFireModel deletes that pointer unconditionally. + // Iso is on the default path — it is what tests/python and test_wheel.py + // run — so this has to be safe. + // + // Nothing deletes a model in a normal run: FireDomain keeps them in + // propModelsTable and fluxModelsTable and never frees either, so the + // destructors below are reached only from here. That is also why this case + // covers only the property-less models: the ones that do allocate delete + // `properties` in their own destructor *and* inherit the base class doing + // it again, so destroying them is a double free. See tests/unit/README.md. + ModelSandbox sandbox; + + PropagationModel* iso = sandbox.propagation("Iso"); + REQUIRE(iso != 0); + REQUIRE(iso->numProperties == 0); + delete iso; + + FluxModel* heat = sandbox.flux("heatFluxBasic"); + REQUIRE(heat != 0); + REQUIRE(heat->numProperties == 0); + delete heat; +} + +TEST_CASE("property registration order is stable within a model") { + // The property array is addressed by position, so two instances of the + // same model must number their properties identically — otherwise a + // DataBroker filling the array for one would mis-feed the other. + ModelSandbox sandbox; + PropagationModel* first = sandbox.propagation("Rothermel"); + PropagationModel* second = sandbox.propagation("Rothermel"); + REQUIRE(first != 0); + REQUIRE(second != 0); + CHECK(first->wantedProperties == second->wantedProperties); + CHECK(first->numProperties == second->numProperties); +} + +} /* TEST_SUITE */ diff --git a/tests/unit/test_propagation_models.cpp b/tests/unit/test_propagation_models.cpp new file mode 100644 index 00000000..cac7d21c --- /dev/null +++ b/tests/unit/test_propagation_models.cpp @@ -0,0 +1,280 @@ +/** + * @file test_propagation_models.cpp + * @brief Rate-of-spread models, tested one call at a time. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + * + * getSpeedForNode splits into "gather the properties from the simulation" and + * "do the arithmetic". Only the second half is physics, and it is a function + * of a plain double array, so it can be checked without a landscape, a fire or + * a time step. + * + * Two kinds of assertion live here. The invariants — no spread without fuel, + * more wind never means less spread — should hold whatever the implementation + * does, and are the ones worth trusting. The pinned values are just the + * current output recorded so a refactor cannot change it unnoticed; they carry + * no claim of being right, only of being what ForeFire produces today. + */ + +#include "doctest/doctest.h" + +#include "model_sandbox.h" + +#include "PropagationModel.h" +#include "SimulationParameters.h" + +#include +#include +#include + +using libforefire::PropagationModel; +using ff_test::Inputs; +using ff_test::ModelSandbox; + +namespace { + +/** Runs a model once with the given inputs. */ +double speedOf(PropagationModel* model, Inputs& inputs) { + return model->getSpeed(inputs.data()); +} + +/** Fuel-driven models whose inputs fillStandardConditions covers. Farsite, + * Lava and RothermelAndrews2018 read their own fuel columns and are covered + * by the registry tests instead. */ +const std::vector& standardFuelModels() { + static std::vector names; + if (!names.empty()) return names; + names.push_back("Balbi2015"); + names.push_back("Balbi2020"); + names.push_back("BalbiNov2011"); + names.push_back("BalbiNov2011Curv"); + names.push_back("BalbiNov2011TMdMl"); + names.push_back("BalbiUnsteady"); + names.push_back("Rothermel"); + return names; +} + +} /* anonymous namespace */ + +TEST_SUITE("propagation models") { + +TEST_CASE("Iso propagates at exactly the configured speed") { + ModelSandbox sandbox; + sandbox.parameters()->setParameter("Iso.speed", "2.5"); + PropagationModel* model = sandbox.propagation("Iso"); + REQUIRE(model != 0); + + Inputs inputs(model); + CHECK(speedOf(model, inputs) == doctest::Approx(2.5)); +} + +TEST_CASE("Iso falls back to 1 m/s when unconfigured") { + // Also the check that a sandbox does not inherit the previous one's + // parameters: the case above sets Iso.speed on its own instance. + ModelSandbox sandbox; + PropagationModel* model = sandbox.propagation("Iso"); + REQUIRE(model != 0); + + Inputs inputs(model); + CHECK(speedOf(model, inputs) == doctest::Approx(1.0)); +} + +TEST_CASE("WindDriven is linear in the normal wind") { + ModelSandbox sandbox; + PropagationModel* model = sandbox.propagation("WindDriven"); + REQUIRE(model != 0); + + Inputs inputs(model); + inputs.set("fuel.vv_coeff", 0.5); + + inputs.set("normalWind", 0.0); + CHECK(speedOf(model, inputs) == doctest::Approx(0.0)); + + inputs.set("normalWind", 4.0); + CHECK(speedOf(model, inputs) == doctest::Approx(2.0)); + + inputs.set("normalWind", 8.0); + CHECK(speedOf(model, inputs) == doctest::Approx(4.0)); +} + +TEST_CASE("no fuel depth means no spread") { + // Rothermel divides the fuel load by the depth to get a bulk density, and + // Balbi2020 does the same for its packing ratio, so both have to special + // case an empty cell rather than divide by zero. A NaN here would travel + // straight into the front geometry. + ModelSandbox sandbox; + const std::vector& names = standardFuelModels(); + for (size_t i = 0; i < names.size(); i++) { + CAPTURE(names[i]); + PropagationModel* model = sandbox.propagation(names[i]); + REQUIRE(model != 0); + + Inputs inputs(model); + REQUIRE(ff_test::fillStandardConditions(inputs, model)); + inputs.set("fuel.e", 0.0); + inputs.set("normalWind", 5.0); + + const double ros = speedOf(model, inputs); + CHECK(std::isfinite(ros)); + CHECK(ros == doctest::Approx(0.0)); + } +} + +TEST_CASE("spread is finite and non-negative over a wind and slope sweep") { + ModelSandbox sandbox; + const std::vector& names = standardFuelModels(); + const double winds[] = {-10.0, -1.0, 0.0, 1.0, 5.0, 20.0, 50.0}; + const double slopes[] = {-1.0, -0.2, 0.0, 0.2, 1.0}; + + for (size_t i = 0; i < names.size(); i++) { + PropagationModel* model = sandbox.propagation(names[i]); + REQUIRE(model != 0); + Inputs inputs(model); + REQUIRE(ff_test::fillStandardConditions(inputs, model)); + + for (size_t w = 0; w < sizeof(winds) / sizeof(winds[0]); w++) { + for (size_t s = 0; s < sizeof(slopes) / sizeof(slopes[0]); s++) { + CAPTURE(names[i]); + CAPTURE(winds[w]); + CAPTURE(slopes[s]); + inputs.set("normalWind", winds[w]); + inputs.set("slope", slopes[s]); + + const double ros = speedOf(model, inputs); + CHECK(std::isfinite(ros)); + CHECK(ros >= 0.0); + } + } + } +} + +TEST_CASE("Rothermel never spreads slower for more wind") { + ModelSandbox sandbox; + PropagationModel* model = sandbox.propagation("Rothermel"); + REQUIRE(model != 0); + + Inputs inputs(model); + REQUIRE(ff_test::fillStandardConditions(inputs, model)); + + double previous = -1.0; + for (double wind = 0.0; wind <= 30.0; wind += 0.5) { + CAPTURE(wind); + inputs.set("normalWind", wind); + const double ros = speedOf(model, inputs); + CHECK(ros >= previous); + previous = ros; + } +} + +TEST_CASE("Rothermel clamps the wind at its own effective limit") { + // The 2013 Andrews/Cruz/Rothermel limit caps the wind the model will react + // to, so beyond it extra wind must change nothing at all. + ModelSandbox sandbox; + PropagationModel* model = sandbox.propagation("Rothermel"); + REQUIRE(model != 0); + + Inputs inputs(model); + REQUIRE(ff_test::fillStandardConditions(inputs, model)); + + inputs.set("normalWind", 200.0); + const double atLimit = speedOf(model, inputs); + inputs.set("normalWind", 2000.0); + const double wellPast = speedOf(model, inputs); + + CHECK(std::isfinite(atLimit)); + CHECK(wellPast == doctest::Approx(atLimit)); +} + +TEST_CASE("Rothermel treats a downslope as flat ground") { + // phiP squares the slope, so without the clamp a downslope would speed the + // fire up exactly as much as the matching upslope. + ModelSandbox sandbox; + PropagationModel* model = sandbox.propagation("Rothermel"); + REQUIRE(model != 0); + + Inputs inputs(model); + REQUIRE(ff_test::fillStandardConditions(inputs, model)); + + inputs.set("slope", 0.0); + const double flat = speedOf(model, inputs); + inputs.set("slope", -0.5); + const double downhill = speedOf(model, inputs); + inputs.set("slope", 0.5); + const double uphill = speedOf(model, inputs); + + CHECK(downhill == doctest::Approx(flat)); + CHECK(uphill > flat); +} + +TEST_CASE("drier live fuel spreads faster, over the physical range") { + // Live fuel moisture appears in BalbiNov2011 through xsi, which scales the + // flame temperature. Raising it should slow the fire down. + // + // It only does so up to about Ml = 0.8 for this fuel. Past that, xsi + // exceeds 1, the flame temperature term goes negative, and R00 raises it + // to the fourth power — so spread starts climbing again: 1.3e-3 m/s at + // Ml = 0.5, 1.9e-8 at Ml = 0.8, then back up to 8.2e-5 at Ml = 1.0, which + // is the value fuel 1 of tests/runff/fuels.csv carries. The sweep below + // deliberately stops before that inversion rather than asserting it is + // correct; see tests/unit/README.md. + ModelSandbox sandbox; + PropagationModel* model = sandbox.propagation("BalbiNov2011"); + REQUIRE(model != 0); + + Inputs inputs(model); + REQUIRE(ff_test::fillStandardConditions(inputs, model)); + inputs.set("normalWind", 5.0); + + double previous = -1.0; + for (double liveMoisture = 0.8; liveMoisture >= 0.1; liveMoisture -= 0.05) { + CAPTURE(liveMoisture); + inputs.set("fuel.Ml", liveMoisture); + const double ros = speedOf(model, inputs); + CHECK(ros >= previous); + previous = ros; + } +} + +TEST_CASE("recorded rates of spread for the standard fuel") { + // Regression pins, not reference physics: fuel 1 of tests/runff/fuels.csv, + // 5 m/s of normal wind on flat ground, with the default + // windReductionFactor of 0.4. If one of these moves, the model changed — + // which may well be intended, but should be a decision rather than a + // surprise. tests/runff only ever runs Rothermel, so for every other model + // here this is the only thing watching the arithmetic. + ModelSandbox sandbox; + + struct Expectation { + const char* model; + double ros; + }; + const Expectation expected[] = { + {"Rothermel", 0.369131}, + {"Balbi2015", 0.069659}, + {"Balbi2020", 0.0783659}, + {"BalbiNov2011", 8.16139e-05}, + {"BalbiNov2011Curv", 0.080263}, + {"BalbiNov2011TMdMl", 0.0584482}, + {"BalbiUnsteady", 0.1}, + }; + + // Loose enough to survive -march=native and FP contraction differing + // between CI runners, tight enough that no real change to a model slips + // through. + const double tolerance = 1e-5; + + for (size_t i = 0; i < sizeof(expected) / sizeof(expected[0]); i++) { + CAPTURE(std::string(expected[i].model)); + PropagationModel* model = sandbox.propagation(expected[i].model); + REQUIRE(model != 0); + + Inputs inputs(model); + REQUIRE(ff_test::fillStandardConditions(inputs, model)); + inputs.set("normalWind", 5.0); + inputs.set("slope", 0.0); + + CHECK(speedOf(model, inputs) == doctest::Approx(expected[i].ros).epsilon(tolerance)); + } +} + +} /* TEST_SUITE */ diff --git a/third_party/README.md b/third_party/README.md new file mode 100644 index 00000000..a9bcd837 --- /dev/null +++ b/third_party/README.md @@ -0,0 +1,24 @@ +# Vendored third-party code + +Dependencies that are checked in rather than fetched, so that a build and a CI +run need no network access beyond cloning the repository. + +Nothing here is compiled into `libforefireL`: `CMakeLists.txt` globs `src/*.cpp` +only, and this directory is on the include path of the test target alone. + +| Directory | Version | License | +| --- | --- | --- | +| `doctest/` | 2.5.3 (2026-07-06) | MIT | + +## doctest + +The unit-test framework, used by `tests/unit/`. A single header, taken +unmodified from +. + +The 2.5 series gates its C++17 features behind `DOCTEST_CPLUSPLUS` checks, so +it still builds under the project's `CMAKE_CXX_STANDARD 11`. + +To update it, replace the file with a newer tagged release and run +`ctest --test-dir build`. Nothing in `tests/unit/` uses anything beyond +`TEST_SUITE`, `TEST_CASE`, `CHECK`, `REQUIRE`, `CAPTURE` and `doctest::Approx`. diff --git a/third_party/doctest/doctest.h b/third_party/doctest/doctest.h new file mode 100644 index 00000000..0b36ccaf --- /dev/null +++ b/third_party/doctest/doctest.h @@ -0,0 +1,9148 @@ +// ============================================================= +// == DO NOT MODIFY THIS FILE BY HAND - IT IS AUTO GENERATED! == +// ============================================================= +// +// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD +// +// Copyright (c) 2016-2023 Viktor Kirilov +// +// Distributed under the MIT Software License +// See accompanying file LICENSE.txt or copy at +// https://opensource.org/licenses/MIT +// +// The documentation can be found at the library's page: +// https://github.com/doctest/doctest/blob/master/doc/markdown/readme.md +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= +// +// The library is heavily influenced by Catch - https://github.com/catchorg/Catch2 +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/catchorg/Catch2/blob/master/LICENSE.txt +// +// The concept of subcases (sections in Catch) and expression decomposition are from there. +// Some parts of the code are taken directly: +// - stringification - the detection of "ostream& operator<<(ostream&, const T&)" and StringMaker<> +// - the Approx() helper class for floating point comparison +// - colors in the console +// - breaking into a debugger +// - signal / SEH handling +// - timer +// - XmlWriter class - thanks to Phil Nash for allowing the direct reuse (AKA copy/paste) +// +// The expression decomposing templates are taken from lest - https://github.com/martinmoene/lest +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/martinmoene/lest/blob/master/LICENSE.txt +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= + +#ifndef DOCTEST_LIBRARY_INCLUDED +#define DOCTEST_LIBRARY_INCLUDED + +// ================================================================================================= +// == VERSION ====================================================================================== +// ================================================================================================= + +#ifndef DOCTEST_PARTS_PUBLIC_VERSION +#define DOCTEST_PARTS_PUBLIC_VERSION + +// NOLINTBEGIN(cppcoreguidelines-macro-to-enum, modernize-macro-to-enum) +#define DOCTEST_VERSION_MAJOR 2 +#define DOCTEST_VERSION_MINOR 5 +#define DOCTEST_VERSION_PATCH 3 +// NOLINTEND(cppcoreguidelines-macro-to-enum, modernize-macro-to-enum) + +// util we need here +#define DOCTEST_TOSTR_IMPL(x) #x +#define DOCTEST_TOSTR(x) DOCTEST_TOSTR_IMPL(x) + +// clang-format off +#define DOCTEST_VERSION_STR \ + DOCTEST_TOSTR(DOCTEST_VERSION_MAJOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_MINOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_PATCH) +// clang-format on + +#define DOCTEST_VERSION (DOCTEST_VERSION_MAJOR * 10000 + DOCTEST_VERSION_MINOR * 100 + DOCTEST_VERSION_PATCH) + +#endif // DOCTEST_PARTS_PUBLIC_VERSION +// ================================================================================================= +// == COMPILER VERSION ============================================================================= +// ================================================================================================= + +#ifndef DOCTEST_PARTS_PUBLIC_COMPILER +#define DOCTEST_PARTS_PUBLIC_COMPILER + +// ideas for the version stuff are taken from here: https://github.com/cxxstuff/cxx_detect + +#ifdef _MSC_VER +#define DOCTEST_CPLUSPLUS _MSVC_LANG +#else +#define DOCTEST_CPLUSPLUS __cplusplus +#endif + +#define DOCTEST_COMPILER(MAJOR, MINOR, PATCH) ((MAJOR) * 10000000 + (MINOR) * 100000 + (PATCH)) + +// GCC/Clang and GCC/MSVC are mutually exclusive, but Clang/MSVC are not because of clang-cl... +#if defined(_MSC_VER) && defined(_MSC_FULL_VER) +#if _MSC_VER == _MSC_FULL_VER / 10000 +#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000) +#else // MSVC +#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 100, _MSC_FULL_VER % 100000) +#endif // MSVC +#endif // MSVC +#if defined(__clang__) && defined(__clang_minor__) && defined(__clang_patchlevel__) +#define DOCTEST_CLANG DOCTEST_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__) +#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && !defined(__INTEL_COMPILER) +#define DOCTEST_GCC DOCTEST_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#endif // GCC +#if defined(__INTEL_COMPILER) +#define DOCTEST_ICC DOCTEST_COMPILER(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif // ICC + +#ifndef DOCTEST_MSVC +#define DOCTEST_MSVC 0 +#endif // DOCTEST_MSVC +#ifndef DOCTEST_CLANG +#define DOCTEST_CLANG 0 +#endif // DOCTEST_CLANG +#ifndef DOCTEST_GCC +#define DOCTEST_GCC 0 +#endif // DOCTEST_GCC +#ifndef DOCTEST_ICC +#define DOCTEST_ICC 0 +#endif // DOCTEST_ICC + +#endif // DOCTEST_PARTS_PUBLIC_COMPILER +// ================================================================================================= +// == COMPILER WARNINGS HELPERS ==================================================================== +// ================================================================================================= + +#ifndef DOCTEST_PARTS_PUBLIC_WARNINGS +#define DOCTEST_PARTS_PUBLIC_WARNINGS + + +#if DOCTEST_CLANG && !DOCTEST_ICC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH _Pragma("clang diagnostic push") +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(clang diagnostic ignored w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP _Pragma("clang diagnostic pop") +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING(w) +#else // DOCTEST_CLANG +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_CLANG + +#if DOCTEST_GCC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH _Pragma("GCC diagnostic push") +#define DOCTEST_GCC_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(GCC diagnostic ignored w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP _Pragma("GCC diagnostic pop") +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING(w) +#else // DOCTEST_GCC +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH +#define DOCTEST_GCC_SUPPRESS_WARNING(w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_GCC + +#if DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH __pragma(warning(push)) +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) __pragma(warning(disable : w)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP __pragma(warning(pop)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) DOCTEST_MSVC_SUPPRESS_WARNING_PUSH DOCTEST_MSVC_SUPPRESS_WARNING(w) +#else // DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_MSVC + +// ================================================================================================= +// == COMPILER WARNINGS ============================================================================ +// ================================================================================================= + +// both the header and the implementation suppress all of these, +// so it only makes sense to aggregate them like so +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-warning-option") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunsafe-buffer-usage") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-macros") \ + \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") \ + \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + /* these 4 also disabled globally via cmake: */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4514) /* unreferenced inline function has been removed */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4571) /* SEH related */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4710) /* function not inlined */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4711) /* function selected for inline expansion*/ \ + /* common ones */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4616) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4619) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4996) /* The compiler encountered a deprecated declaration */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4706) /* assignment within conditional expression */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4512) /* 'class' : assignment operator could not be generated */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4127) /* conditional expression is constant */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4640) /* construction of local static object not thread-safe */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5264) /* 'variable-name': 'const' variable is not used */ \ + /* static analysis */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26439) /* Function may not throw. Declare it 'noexcept' */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26495) /* Always initialize a member variable */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26451) /* Arithmetic overflow ... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26444) /* Avoid unnamed objects with custom ctor and dtor... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26812) /* Prefer 'enum class' over 'enum' */ + +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#define DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH \ + DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ + \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wnon-virtual-dtor") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wdeprecated") \ + \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wctor-dtor-privacy") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wnon-virtual-dtor") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-promo") \ + \ + DOCTEST_MSVC_SUPPRESS_WARNING(4623) /* default constructor was implicitly deleted */ + +#define DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#define DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH \ + DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ + \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wglobal-constructors") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wshorten-64-to-32") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-variable-declarations") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-noreturn") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wdisabled-macro-expansion") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-braces") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-field-initializers") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-function") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wnrvo") \ + \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-field-initializers") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-braces") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-default") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunsafe-loop-optimizations") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wold-style-cast") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-function") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmultiple-inheritance") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wsuggest-attribute") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wnrvo") \ + \ + DOCTEST_MSVC_SUPPRESS_WARNING(4267) /* conversion from 'x' to 'y', possible loss of data */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4530) /* exception handler, but unwind semantics not enabled */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4577) /* 'noexcept' with no exception handling mode specified */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4774) /* format string in argument is not a string literal */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4365) /* signed/unsigned mismatch */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5039) /* pointer to pot. throwing function passed to extern C */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4800) /* forcing value to bool (performance warning) */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5245) /* unreferenced function with internal linkage removed */ + +#define DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_WARNINGS + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +// ================================================================================================= +// == FEATURE DETECTION ============================================================================ +// ================================================================================================= + +#ifndef DOCTEST_PARTS_PUBLIC_CONFIG +#define DOCTEST_PARTS_PUBLIC_CONFIG + +#ifndef DOCTEST_PARTS_PUBLIC_PLATFORM +#define DOCTEST_PARTS_PUBLIC_PLATFORM + +#if defined(__APPLE__) +// Apple detection taken from Catch2 codebase +// For information: +// https://github.com/swiftlang/swift-corelibs-foundation/blob/release/5.10/CoreFoundation/Base.subproj/SwiftRuntime/TargetConditionals.h +#include +#if (defined(TARGET_OS_MAC) && TARGET_OS_MAC == 1) || (defined(TARGET_OS_OSX) && TARGET_OS_OSX == 1) +#define DOCTEST_PLATFORM_MAC + +#elif defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1 +#define DOCTEST_PLATFORM_IPHONE +#endif + +#elif defined(WIN32) || defined(_WIN32) +#define DOCTEST_PLATFORM_WINDOWS + +#elif defined(__wasi__) +#define DOCTEST_PLATFORM_WASI + +#else // defined(linux) || defined(__linux) // defined(__linux__) +#define DOCTEST_PLATFORM_LINUX +#endif // DOCTEST_PLATFORM + +#endif // DOCTEST_PARTS_PUBLIC_PLATFORM + +// general compiler feature support table: https://en.cppreference.com/w/cpp/compiler_support +// MSVC C++11 feature support table: https://msdn.microsoft.com/en-us/library/hh567368.aspx +// GCC C++11 feature support table: https://gcc.gnu.org/projects/cxx-status.html +// MSVC version table: +// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering +// MSVC++ 14.3 (17) _MSC_VER == 1930 (Visual Studio 2022) +// MSVC++ 14.2 (16) _MSC_VER == 1920 (Visual Studio 2019) +// MSVC++ 14.1 (15) _MSC_VER == 1910 (Visual Studio 2017) +// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015) +// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013) +// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012) +// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010) +// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008) +// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005) + +// Universal Windows Platform support +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#ifndef DOCTEST_CONFIG_NO_WINDOWS_SEH +#define DOCTEST_CONFIG_NO_WINDOWS_SEH +#endif +#ifndef DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS +#define DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS +#endif +#endif // defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#if DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +#define DOCTEST_CONFIG_WINDOWS_SEH +#endif // DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +#if defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH) +#undef DOCTEST_CONFIG_WINDOWS_SEH +#endif // defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH) + +#if !defined(_WIN32) && !defined(__QNX__) && !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(__EMSCRIPTEN__) && \ + !defined(__wasi__) +#define DOCTEST_CONFIG_POSIX_SIGNALS +#endif // _WIN32 +#if defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) && defined(DOCTEST_CONFIG_POSIX_SIGNALS) +#undef DOCTEST_CONFIG_POSIX_SIGNALS +#endif // DOCTEST_CONFIG_NO_POSIX_SIGNALS + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) || defined(__wasi__) +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // no exceptions +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#if defined(DOCTEST_CONFIG_NO_EXCEPTIONS) && !defined(DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS) +#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS && !DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef __wasi__ +#define DOCTEST_CONFIG_NO_MULTITHREADING +#endif + +#if defined(DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) && !defined(DOCTEST_CONFIG_IMPLEMENT) +#define DOCTEST_CONFIG_IMPLEMENT +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +#if defined(_WIN32) || defined(__CYGWIN__) +#if DOCTEST_MSVC +#define DOCTEST_SYMBOL_EXPORT __declspec(dllexport) +#define DOCTEST_SYMBOL_IMPORT __declspec(dllimport) +#else // MSVC +#define DOCTEST_SYMBOL_EXPORT __attribute__((dllexport)) +#define DOCTEST_SYMBOL_IMPORT __attribute__((dllimport)) +#endif // MSVC +#else // _WIN32 +#define DOCTEST_SYMBOL_EXPORT __attribute__((visibility("default"))) +#define DOCTEST_SYMBOL_IMPORT +#endif // _WIN32 + +#ifdef DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#ifdef DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_EXPORT +#else // DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_IMPORT +#endif // DOCTEST_CONFIG_IMPLEMENT +#else // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#define DOCTEST_INTERFACE +#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL + +// needed for extern template instantiations +// see https://github.com/fmtlib/fmt/issues/2228 +#if DOCTEST_MSVC +#define DOCTEST_INTERFACE_DECL +#define DOCTEST_INTERFACE_DEF DOCTEST_INTERFACE +#else // DOCTEST_MSVC +#define DOCTEST_INTERFACE_DECL DOCTEST_INTERFACE +#define DOCTEST_INTERFACE_DEF +#endif // DOCTEST_MSVC + +#define DOCTEST_EMPTY + +#if DOCTEST_MSVC +#define DOCTEST_NOINLINE __declspec(noinline) +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#elif DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 5, 0) +#define DOCTEST_NOINLINE +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#else +#define DOCTEST_NOINLINE __attribute__((noinline)) +#define DOCTEST_UNUSED __attribute__((unused)) +#define DOCTEST_ALIGNMENT(x) __attribute__((aligned(x))) +#endif + +#ifdef DOCTEST_CONFIG_NO_CONTRADICTING_INLINE +#define DOCTEST_INLINE_NOINLINE inline +#else +#define DOCTEST_INLINE_NOINLINE inline DOCTEST_NOINLINE +#endif + +#ifndef DOCTEST_NORETURN +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NORETURN +#else // DOCTEST_MSVC +#define DOCTEST_NORETURN [[noreturn]] +#endif // DOCTEST_MSVC +#endif // DOCTEST_NORETURN + +#ifndef DOCTEST_NOEXCEPT +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NOEXCEPT +#else // DOCTEST_MSVC +#define DOCTEST_NOEXCEPT noexcept +#endif // DOCTEST_MSVC +#endif // DOCTEST_NOEXCEPT + +#ifndef DOCTEST_CONSTEXPR +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_CONSTEXPR const +#define DOCTEST_CONSTEXPR_FUNC inline +#else // DOCTEST_MSVC +#define DOCTEST_CONSTEXPR constexpr +#define DOCTEST_CONSTEXPR_FUNC constexpr +#endif // DOCTEST_MSVC +#endif // DOCTEST_CONSTEXPR + +#ifndef DOCTEST_NO_SANITIZE_INTEGER +#if DOCTEST_CLANG >= DOCTEST_COMPILER(3, 7, 0) +#define DOCTEST_NO_SANITIZE_INTEGER __attribute__((no_sanitize("integer"))) +#else +#define DOCTEST_NO_SANITIZE_INTEGER +#endif +#endif // DOCTEST_NO_SANITIZE_INTEGER + +// this is kept here for backwards compatibility since the config option was changed +#ifdef DOCTEST_CONFIG_USE_IOSFWD +#ifndef DOCTEST_CONFIG_USE_STD_HEADERS +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif +#endif // DOCTEST_CONFIG_USE_IOSFWD + +// for clang - always include or (which drags some std stuff) +// because we want to check if we are using libc++ with the _LIBCPP_VERSION macro in +// which case we don't want to forward declare stuff from std - for reference: +// https://github.com/doctest/doctest/issues/126 +// https://github.com/doctest/doctest/issues/356 +#if DOCTEST_CLANG +#if DOCTEST_CPLUSPLUS >= 201703L && __has_include() +#include +#else +#include +#endif +#endif // clang + +#ifdef _LIBCPP_VERSION +#ifndef DOCTEST_CONFIG_USE_STD_HEADERS +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif +#endif // _LIBCPP_VERSION + +#ifdef DOCTEST_CONFIG_USE_STD_HEADERS +#ifndef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#endif // DOCTEST_CONFIG_USE_STD_HEADERS + +#if defined(__has_builtin) +#define DOCTEST_HAS_BUILTIN(x) __has_builtin(x) +#else +#define DOCTEST_HAS_BUILTIN(x) 0 +#endif // __has_builtin + +#endif // DOCTEST_PARTS_PUBLIC_CONFIG + +// ================================================================================================= +// == FEATURE DETECTION END ======================================================================== +// ================================================================================================= +#ifndef DOCTEST_PARTS_PUBLIC_UTILITY +#define DOCTEST_PARTS_PUBLIC_UTILITY + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#define DOCTEST_DECLARE_INTERFACE(name) \ + virtual ~name(); \ + name() = default; \ + name(const name &) = delete; \ + name(name &&) = delete; \ + name &operator=(const name &) = delete; \ + name &operator=(name &&) = delete; + +#define DOCTEST_DEFINE_INTERFACE(name) name::~name() = default; + +#if !defined(DOCTEST_COUNTER) +#if DOCTEST_CLANG >= DOCTEST_COMPILER(22, 0, 0) +#define DOCTEST_COUNTER __LINE__ +#elif defined(__COUNTER__) +#define DOCTEST_COUNTER __COUNTER__ +#else +#define DOCTEST_COUNTER __LINE__ +#endif +#endif // defined(DOCTEST_COUNTER) + +// internal macros for string concatenation and anonymous variable name generation +#define DOCTEST_CAT_IMPL(s1, s2) s1##s2 +#define DOCTEST_CAT(s1, s2) DOCTEST_CAT_IMPL(s1, s2) +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, DOCTEST_COUNTER) + +#ifndef DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x & +#else // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x +#endif // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE + +namespace doctest { +namespace detail { +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-function") +static DOCTEST_CONSTEXPR int consume(const int *, int) noexcept { + return 0; +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +} // namespace detail +} // namespace doctest + +#define DOCTEST_GLOBAL_NO_WARNINGS(var, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wglobal-constructors") \ + static const int var = doctest::detail::consume(&var, __VA_ARGS__); \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_UTILITY +#ifndef DOCTEST_PARTS_PUBLIC_DEBUGGER +#define DOCTEST_PARTS_PUBLIC_DEBUGGER + + +#ifndef DOCTEST_BREAK_INTO_DEBUGGER + +// should probably take a look at https://github.com/scottt/debugbreak +#if DOCTEST_CLANG && DOCTEST_HAS_BUILTIN(__builtin_debugtrap) +#define DOCTEST_BREAK_INTO_DEBUGGER() __builtin_debugtrap() + +#elif defined(DOCTEST_PLATFORM_LINUX) +#if defined(__GNUC__) && (defined(__i386) || defined(__x86_64)) +// Break at the location of the failing check if possible +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" ::) // NOLINT(hicpp-no-assembler) +#else +#include +#define DOCTEST_BREAK_INTO_DEBUGGER() raise(SIGTRAP) +#endif + +#elif defined(DOCTEST_PLATFORM_MAC) +#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64__) || defined(__i386) +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" ::) // NOLINT(hicpp-no-assembler) +#elif defined(__ppc__) || defined(__ppc64__) +// https://www.cocoawithlove.com/2008/03/break-into-debugger.html +#define DOCTEST_BREAK_INTO_DEBUGGER() /* NOLINTNEXTLINE(hicpp-no-assembler) */ \ + __asm__("li r0, 20\nsc\nnop\nli r0, 37\nli r4, 2\nsc\nnop\n" ::: "memory", "r0", "r3", "r4") +#else +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("brk #0"); // NOLINT(hicpp-no-assembler) +#endif + +#elif DOCTEST_MSVC +#define DOCTEST_BREAK_INTO_DEBUGGER() __debugbreak() + +#elif defined(__MINGW32__) +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wredundant-decls") +extern "C" __declspec(dllimport) void __stdcall DebugBreak(); +DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_BREAK_INTO_DEBUGGER() ::DebugBreak() +#else // linux +#define DOCTEST_BREAK_INTO_DEBUGGER() (static_cast(0)) +#endif // linux +#endif // DOCTEST_BREAK_INTO_DEBUGGER + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { +DOCTEST_INTERFACE bool isDebuggerActive(); +} // namespace detail +} // namespace doctest + +#endif + +#endif // DOCTEST_PARTS_PUBLIC_DEBUGGER +#ifndef DOCTEST_PARTS_PUBLIC_STD_FWD +#define DOCTEST_PARTS_PUBLIC_STD_FWD + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifdef DOCTEST_CONFIG_USE_STD_HEADERS +#include +#include +#include +#else // DOCTEST_CONFIG_USE_STD_HEADERS + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4643) // Forward declaring 'X' in namespace std is not permitted by the C++ Standard. +DOCTEST_MSVC_SUPPRESS_WARNING(5285) // Cannot declare a specialization for 'template name': template argument. + +// NOLINTBEGIN(bugprone-std-namespace-modification, cert-dcl58-cpp) +namespace std { +typedef decltype(nullptr) nullptr_t; // NOLINT(modernize-use-using) +typedef decltype(sizeof(void *)) size_t; // NOLINT(modernize-use-using) +template +struct char_traits; +template <> +struct char_traits; +template +class basic_ostream; // NOLINT(fuchsia-virtual-inheritance) +typedef basic_ostream> ostream; // NOLINT(modernize-use-using) +template +// NOLINTNEXTLINE +basic_ostream &operator<<(basic_ostream &, const char *); +template +class basic_istream; +typedef basic_istream> istream; // NOLINT(modernize-use-using) +template +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(5285) // Specializing template std::tuple is forbidden +class tuple; +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +template +class allocator; +template +class basic_string; +using string = basic_string, allocator>; +#endif // VS 2019 +} // namespace std +// NOLINTEND(bugprone-std-namespace-modification, cert-dcl58-cpp) + +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_USE_STD_HEADERS + +namespace doctest { +using std::size_t; +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_STD_FWD +#ifndef DOCTEST_PARTS_PUBLIC_STD_TYPE_TRAITS +#define DOCTEST_PARTS_PUBLIC_STD_TYPE_TRAITS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#include +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + +namespace doctest { +namespace detail { +namespace types { + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +using namespace std; +#else +template +struct enable_if {}; + +template +struct enable_if { + using type = T; +}; + +struct true_type { + static DOCTEST_CONSTEXPR bool value = true; +}; + +struct false_type { + static DOCTEST_CONSTEXPR bool value = false; +}; + +template +struct remove_reference { + using type = T; +}; + +template +struct remove_reference { + using type = T; +}; + +template +struct remove_reference { + using type = T; +}; + +template +struct is_same : false_type {}; + +template +struct is_same : true_type {}; + +template +struct is_rvalue_reference : false_type {}; + +template +struct is_rvalue_reference : true_type {}; + +template +struct remove_const { + using type = T; +}; + +template +struct remove_const { + using type = T; +}; + +// Compiler intrinsics +template +struct is_enum { + static DOCTEST_CONSTEXPR bool value = __is_enum(T); +}; + +template +struct underlying_type { + using type = __underlying_type(T); +}; + +template +struct is_pointer : false_type {}; + +template +struct is_pointer : true_type {}; + +template +struct is_array : false_type {}; +// NOLINTNEXTLINE(*-avoid-c-arrays) +template +struct is_array : true_type {}; +#endif + +#if !(defined(DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS) && (DOCTEST_CPLUSPLUS >= 201703L)) +template +using void_t = void; +#endif + +} // namespace types +} // namespace detail +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_STD_TYPE_TRAITS +#ifndef DOCTEST_PARTS_PUBLIC_STD_UTILITY +#define DOCTEST_PARTS_PUBLIC_STD_UTILITY + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +// +template +T &&declval(); + +template +DOCTEST_CONSTEXPR_FUNC T &&forward(typename types::remove_reference::type &t) DOCTEST_NOEXCEPT { + return static_cast(t); +} + +template +// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) +DOCTEST_CONSTEXPR_FUNC T &&forward(typename types::remove_reference::type &&t) DOCTEST_NOEXCEPT { + return static_cast(t); +} + +template +struct deferred_false : types::false_type {}; + +} // namespace detail +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_STD_UTILITY +#ifndef DOCTEST_PARTS_PUBLIC_STRING +#define DOCTEST_PARTS_PUBLIC_STRING + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +#ifndef DOCTEST_CONFIG_STRING_SIZE_TYPE +#define DOCTEST_CONFIG_STRING_SIZE_TYPE unsigned +#endif + +namespace detail { + +template +struct is_std_string : types::false_type {}; + +template +struct is_std_string< + T, + typename types::enable_if< + types::is_same().c_str()), const char *>::value && + types::is_same().size()), size_t>::value>::type> : types::true_type {}; + +} // namespace detail + +// A 24 byte string class (can be as small as 17 for x64 and 13 for x86) that can hold strings +// with length of up to 23 chars on the stack before going on the heap - +// the last byte of the buffer is used for: +// - "is small" bit - the highest bit - if "0" then it is small - otherwise its "1" (128) +// - if small - capacity left before going on the heap - using the lowest 5 bits +// - if small - 2 bits are left unused - the second and third highest ones +// - if small - acts as a null terminator if strlen() is 23 (24 including the null terminator) +// and the "is small" bit remains "0" ("as well as the capacity left") so its OK +// Idea taken from this lecture about the string implementation of facebook/folly - fbstring +// https://www.youtube.com/watch?v=kPR8h4-qZdk +// TODO: +// - optimizations - like not deleting memory unnecessarily in operator= and etc. +// - resize/reserve/clear +// - replace +// - back/front +// - iterator stuff +// - find & friends +// - push_back/pop_back +// - assign/insert/erase +// - relational operators as free functions - taking const char* as one of the params +class DOCTEST_INTERFACE String { +public: + using size_type = DOCTEST_CONFIG_STRING_SIZE_TYPE; + +private: + static DOCTEST_CONSTEXPR size_type len = 24; + static DOCTEST_CONSTEXPR size_type last = len - 1; + + struct view // len should be more than sizeof(view) - because of the final byte for flags + { + char *ptr; + size_type size; + size_type capacity; + }; + + union { + char buf[len]; // NOLINT(*-avoid-c-arrays) + view data; + }; + + char *allocate(size_type sz); + + bool isOnStack() const noexcept { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) + return (buf[last] & 128) == 0; + } + + void setOnHeap() noexcept; + void setLast(size_type in = last) noexcept; + void setSize(size_type sz) noexcept; + + void copy(const String &other); + +public: + static DOCTEST_CONSTEXPR size_type npos = static_cast(-1); + + String() noexcept; + ~String(); + + String(const char *in); + String(const char *in, size_type in_size); + + String(std::istream &in, size_type in_size); + + template ::value, bool>::type = true> + String(const T &in) + : String(in.c_str(), static_cast(in.size())) {} + + String(const String &other); + String &operator=(const String &other); + + String &operator+=(const String &other); + + String(String &&other) noexcept; + String &operator=(String &&other) noexcept; + + char operator[](size_type i) const; + char &operator[](size_type i); + + // the only functions I'm willing to leave in the interface - available for inlining + const char *c_str() const { + return const_cast(this)->c_str(); // NOLINT + } + + // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) + char *c_str() { + if (isOnStack()) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(buf); + } + return data.ptr; + } + // NOLINTEND(cppcoreguidelines-pro-type-union-access) + + size_type size() const; + size_type capacity() const; + + String substr(size_type pos, size_type cnt = npos) &&; + String substr(size_type pos, size_type cnt = npos) const &; + + size_type find(char ch, size_type pos = 0) const; + size_type rfind(char ch, size_type pos = npos) const; + + int compare(const char *other, bool no_case = false) const; + int compare(const String &other, bool no_case = false) const; + + friend DOCTEST_INTERFACE std::ostream &operator<<(std::ostream &s, const String &in); +}; + +DOCTEST_INTERFACE String operator+(const String &lhs, const String &rhs); + +DOCTEST_INTERFACE bool operator==(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator!=(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator<(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator>(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator<=(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator>=(const String &lhs, const String &rhs); + +namespace detail { + +// MSVS 2015 :( +#if !DOCTEST_CLANG && defined(_MSC_VER) && _MSC_VER <= 1900 +template +struct has_global_insertion_operator : types::false_type {}; + +template +struct has_global_insertion_operator(), declval()), void())> + : types::true_type {}; + +template +struct has_insertion_operator { + static DOCTEST_CONSTEXPR bool value = has_global_insertion_operator::value; +}; + +template +struct insert_hack; + +template +struct insert_hack { + static void insert(std::ostream &os, const T &t) { + ::operator<<(os, t); + } +}; + +template +struct insert_hack { + static void insert(std::ostream &os, const T &t) { + operator<<(os, t); + } +}; + +template +using insert_hack_t = insert_hack::value>; +#else +template +struct has_insertion_operator : types::false_type {}; +#endif + +template +struct has_insertion_operator(), declval()), void())> + : types::true_type {}; + +template +struct should_stringify_as_underlying_type { + static DOCTEST_CONSTEXPR bool value = + detail::types::is_enum::value && !doctest::detail::has_insertion_operator::value; +}; + +template +struct is_pair : types::false_type {}; + +template +struct is_pair< + T, + types::void_t< + typename T::first_type, + typename T::second_type, + decltype(declval().first), + decltype(declval().second)>> : types::true_type {}; + +template +struct is_container : types::false_type {}; + +template +struct is_container< + T, + types::void_t< + typename T::value_type, + typename T::iterator, + decltype(declval().begin()), + decltype(declval().end())>> : types::true_type {}; + +DOCTEST_INTERFACE std::ostream *tlssPush(); +DOCTEST_INTERFACE String tlssPop(); + +template +struct StringMakerBase { + template + static String convert(const DOCTEST_REF_WRAP(T)) { +#ifdef DOCTEST_CONFIG_REQUIRE_STRINGIFICATION_FOR_ALL_USED_TYPES + static_assert(deferred_false::value, "No stringification detected for type T. See string conversion manual"); +#endif + return "{?}"; + } +}; + +template +struct filldata; + +template +void filloss(std::ostream *stream, const T &in) { + filldata::fill(stream, in); +} + +template +void filloss(std::ostream *stream, const T (&in)[N]) { // NOLINT(*-avoid-c-arrays) + // T[N], T(&)[N], T(&&)[N] have same behaviour. + // Hence remove reference. + filloss::type>(stream, in); +} + +template +String toStream(const T &in) { + std::ostream *stream = tlssPush(); + filloss(stream, in); + return tlssPop(); +} + +template <> +struct StringMakerBase { + template + static String convert(const DOCTEST_REF_WRAP(T) in) { + return toStream(in); + } +}; + +} // namespace detail + +template +struct StringMaker + : public detail::StringMakerBase< + detail::has_insertion_operator::value || detail::types::is_pointer::value || + detail::types::is_array::value || detail::is_pair::value || detail::is_container::value> {}; + +#ifndef DOCTEST_STRINGIFY +#ifdef DOCTEST_CONFIG_DOUBLE_STRINGIFY +#define DOCTEST_STRINGIFY(...) toString(toString(__VA_ARGS__)) +#else +#define DOCTEST_STRINGIFY(...) toString(__VA_ARGS__) +#endif +#endif + +template +String toString() { +#if DOCTEST_CLANG == 0 && DOCTEST_GCC == 0 && DOCTEST_ICC == 0 + String ret = __FUNCSIG__; // class doctest::String __cdecl doctest::toString(void) + String::size_type beginPos = ret.find('<'); + return ret.substr(beginPos + 1, ret.size() - beginPos - static_cast(sizeof(">(void)"))); +#else + const String ret = __PRETTY_FUNCTION__; // doctest::String toString() [with T = TYPE] + const String::size_type begin = ret.find('=') + 2; + return ret.substr(begin, ret.size() - begin - 1); +#endif // Compiler +} + +template < + typename T, + typename detail::types::enable_if::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + return StringMaker::convert(value); +} + +// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) +inline String &&toString(String &&in) { + return static_cast(in); +} + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(const char *in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +DOCTEST_INTERFACE String toString(const std::string &in); +#endif // VS 2019 + +DOCTEST_INTERFACE String toString(const String &in); + +DOCTEST_INTERFACE String toString(std::nullptr_t); + +DOCTEST_INTERFACE String toString(bool in); + +DOCTEST_INTERFACE String toString(float in); +DOCTEST_INTERFACE String toString(double in); +DOCTEST_INTERFACE String toString(double long in); + +DOCTEST_INTERFACE String toString(char in); +DOCTEST_INTERFACE String toString(char signed in); +DOCTEST_INTERFACE String toString(char unsigned in); +DOCTEST_INTERFACE String toString(short in); +DOCTEST_INTERFACE String toString(short unsigned in); +DOCTEST_INTERFACE String toString(signed in); +DOCTEST_INTERFACE String toString(unsigned in); +DOCTEST_INTERFACE String toString(long in); +DOCTEST_INTERFACE String toString(long unsigned in); +DOCTEST_INTERFACE String toString(long long in); +DOCTEST_INTERFACE String toString(long long unsigned in); + +template < + typename T, + typename detail::types::enable_if::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + using UT = typename detail::types::underlying_type::type; + return (DOCTEST_STRINGIFY(static_cast(value))); +} + +namespace detail { +template +struct filldata { + static void fill(std::ostream *stream, const T &in) { +#if defined(_MSC_VER) && _MSC_VER <= 1900 + insert_hack_t::insert(*stream, in); +#else + operator<<(*stream, in); +#endif // _MSV_VER + } +}; + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866) +// NOLINTBEGIN(*-avoid-c-arrays) +template +struct filldata { + static void fill(std::ostream *stream, const T (&in)[N]) { + *stream << "["; + for (size_t i = 0; i < N; i++) { + if (i != 0) { + *stream << ", "; + } + *stream << (DOCTEST_STRINGIFY(in[i])); + } + *stream << "]"; + } +}; +// NOLINTEND(*-avoid-c-arrays) +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +// Specialized since we don't want the terminating null byte! +// NOLINTBEGIN(*-avoid-c-arrays) +template +struct filldata { + static void fill(std::ostream *stream, const char (&in)[N]) { + *stream << String(in, in[N - 1] ? N : N - 1); + } // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) +}; +// NOLINTEND(*-avoid-c-arrays) + +template <> +struct filldata { + DOCTEST_INTERFACE static void fill(std::ostream *stream, const void *in); +}; + +template <> +struct filldata { + DOCTEST_INTERFACE static void fill(std::ostream *stream, const volatile void *in); +}; + +template +struct filldata { + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4180) + static void fill(std::ostream *stream, const T *in) { + DOCTEST_MSVC_SUPPRESS_WARNING_POP + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wmicrosoft-cast") + filldata::fill( + stream, +#if DOCTEST_GCC == 0 || DOCTEST_GCC >= DOCTEST_COMPILER(4, 9, 0) + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(in) +#else + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + *reinterpret_cast(&in) +#endif // DOCTEST_GCC + ); + DOCTEST_CLANG_SUPPRESS_WARNING_POP + } +}; + +template +struct filldata< + T, + typename detail::types::enable_if::value && detail::is_pair::value>::type> { + static void fill(std::ostream *stream, const T &in) { + *stream << "{" << DOCTEST_STRINGIFY(in.first) << ", " << DOCTEST_STRINGIFY(in.second) << "}"; + } +}; + +template +struct filldata< + T, + typename detail::types::enable_if< + !detail::has_insertion_operator::value && detail::is_container::value>::type> { + static void fill(std::ostream *stream, const DOCTEST_REF_WRAP(T) in) { + *stream << "{"; + for (auto it = in.begin(); it != in.end(); ++it) { + if (it != in.begin()) { + *stream << ", "; + } + *stream << DOCTEST_STRINGIFY(*it); + } + *stream << "}"; + } +}; + +#ifndef DOCTEST_CONFIG_DISABLE +template +String stringifyBinaryExpr(const DOCTEST_REF_WRAP(L) lhs, const char *op, const DOCTEST_REF_WRAP(R) rhs) { + return (DOCTEST_STRINGIFY(lhs)) + op + (DOCTEST_STRINGIFY(rhs)); +} +#endif // DOCTEST_CONFIG_DISABLE +} // namespace detail + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_STRING +#ifndef DOCTEST_PARTS_PUBLIC_MATCHERS_CONTAINS +#define DOCTEST_PARTS_PUBLIC_MATCHERS_CONTAINS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +class DOCTEST_INTERFACE Contains { +public: + explicit Contains(const String &string); + + bool checkWith(const String &other) const; + + String string; +}; + +DOCTEST_INTERFACE String toString(const Contains &in); + +DOCTEST_INTERFACE bool operator==(const String &lhs, const Contains &rhs); +DOCTEST_INTERFACE bool operator==(const Contains &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator!=(const String &lhs, const Contains &rhs); +DOCTEST_INTERFACE bool operator!=(const Contains &lhs, const String &rhs); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_MATCHERS_CONTAINS +#ifndef DOCTEST_PARTS_PUBLIC_MATCHERS_APPROX +#define DOCTEST_PARTS_PUBLIC_MATCHERS_APPROX + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE Approx { + Approx(double value); + + Approx operator()(double value) const; + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + explicit Approx( + const T &value, + typename detail::types::enable_if::value>::type * = static_cast(nullptr) + ) { + *this = static_cast(value); + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx &epsilon(double newEpsilon); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename std::enable_if::value, Approx &>::type epsilon(const T &newEpsilon) { + m_epsilon = static_cast(newEpsilon); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx &scale(double newScale); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename std::enable_if::value, Approx &>::type scale(const T &newScale) { + m_scale = static_cast(newScale); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format off + DOCTEST_INTERFACE friend bool operator==(double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator==(const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator!=(double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator!=(const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator<=(double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator<=(const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator>=(double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator>=(const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator< (double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator< (const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator> (double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator> (const Approx &lhs, double rhs); + // clang-format on + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_APPROX_PREFIX \ + template \ + friend typename std::enable_if::value, bool>::type + + // clang-format off + DOCTEST_APPROX_PREFIX operator==(const T &lhs, const Approx &rhs) { return operator==(static_cast(lhs), rhs); } + DOCTEST_APPROX_PREFIX operator==(const Approx &lhs, const T &rhs) { return operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator!=(const T &lhs, const Approx &rhs) { return !operator==(lhs, rhs); } + DOCTEST_APPROX_PREFIX operator!=(const Approx &lhs, const T &rhs) { return !operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator<=(const T &lhs, const Approx &rhs) { return static_cast(lhs) < rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator<=(const Approx &lhs, const T &rhs) { return lhs.m_value < static_cast(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const T &lhs, const Approx &rhs) { return static_cast(lhs) > rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const Approx &lhs, const T &rhs) { return lhs.m_value > static_cast(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator< (const T &lhs, const Approx &rhs) { return static_cast(lhs) < rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator< (const Approx &lhs, const T &rhs) { return lhs.m_value < static_cast(rhs) && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const T &lhs, const Approx &rhs) { return static_cast(lhs) > rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const Approx &lhs, const T &rhs) { return lhs.m_value > static_cast(rhs) && lhs != rhs; } + // clang-format off +#undef DOCTEST_APPROX_PREFIX +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + double m_epsilon; + double m_scale; + double m_value; +}; + +DOCTEST_INTERFACE String toString(const Approx &in); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_MATCHERS_APPROX +#ifndef DOCTEST_PARTS_PUBLIC_MATCHERS_IS_NAN +#define DOCTEST_PARTS_PUBLIC_MATCHERS_IS_NAN + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +template +struct DOCTEST_INTERFACE_DECL IsNaN { + F value; + bool flipped; + IsNaN(F f, bool flip = false) + : value(f), flipped(flip) {} + + IsNaN operator!() const { + return {value, !flipped}; + } + + operator bool() const; +}; + +#ifndef __MINGW32__ +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +#endif + +DOCTEST_INTERFACE String toString(IsNaN in); +DOCTEST_INTERFACE String toString(IsNaN in); +DOCTEST_INTERFACE String toString(IsNaN in); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_MATCHERS_IS_NAN +#ifndef DOCTEST_PARTS_PUBLIC_CONTEXT_OPTIONS +#define DOCTEST_PARTS_PUBLIC_CONTEXT_OPTIONS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace detail { +struct DOCTEST_INTERFACE TestCase; +} // namespace detail + +struct ContextOptions { + std::ostream *cout = nullptr; // stdout stream + String binary_name; // the test binary name + + const detail::TestCase *currentTest = nullptr; + + // == parameters from the command line + String out; // output filename + String order_by; // how tests should be ordered + unsigned rand_seed; // the seed for rand ordering + + unsigned first; // the first (matching) test to be executed + unsigned last; // the last (matching) test to be executed + + int abort_after; // stop tests after this many failed assertions + int subcase_filter_levels; // apply the subcase filters for the first N levels + + bool success; // include successful assertions in output + bool case_sensitive; // if filtering should be case sensitive + bool exit; // if the program should be exited after the tests are ran/whatever + bool duration; // print the time duration of each test case + bool minimal; // minimal console output (only test failures) + bool quiet; // no console output + bool no_throw; // to skip exceptions-related assertion macros + bool no_exitcode; // if the framework should return 0 as the exitcode + bool no_run; // to not run the tests at all (can be done with an "*" exclude) + bool no_intro; // to not print the intro of the framework + bool no_version; // to not print the version of the framework + bool no_colors; // if output to the console should be colorized + bool force_colors; // forces the use of colors even when a tty cannot be detected + bool no_breaks; // to not break into the debugger + bool no_skip; // don't skip test cases which are marked to be skipped + bool gnu_file_line; // if line numbers should be surrounded with :x: and not (x): + bool no_path_in_filenames; // if the path to files should be removed from the output + String strip_file_prefixes; // remove the longest matching one of these prefixes from any file paths in the output + bool no_line_numbers; // if source code line numbers should be omitted from the output + bool no_debug_output; // no output in the debug console when a debugger is attached + bool no_skipped_summary; // don't print "skipped" in the summary !!! UNDOCUMENTED !!! + bool no_time_in_output; // omit any time/timestamps from output !!! UNDOCUMENTED !!! + + bool help; // to print the help + bool version; // to print the version + bool count; // if only the count of matching tests is to be retrieved + bool list_test_cases; // to list all tests matching the filters + bool list_test_suites; // to list all suites matching the filters + bool list_reporters; // lists all registered reporters +}; + +DOCTEST_INTERFACE const ContextOptions *getContextOptions(); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_CONTEXT_OPTIONS +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_TYPE +#define DOCTEST_PARTS_PUBLIC_ASSERT_TYPE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace assertType { +enum Enum { + // macro traits + + is_warn = 1, + is_check = 2 * is_warn, + is_require = 2 * is_check, + + is_normal = 2 * is_require, + is_throws = 2 * is_normal, + is_throws_as = 2 * is_throws, + is_throws_with = 2 * is_throws_as, + is_nothrow = 2 * is_throws_with, + + is_false = 2 * is_nothrow, + is_unary = 2 * is_false, // not checked anywhere - used just to distinguish the types + + is_eq = 2 * is_unary, + is_ne = 2 * is_eq, + + is_lt = 2 * is_ne, + is_gt = 2 * is_lt, + + is_ge = 2 * is_gt, + is_le = 2 * is_ge, + + // macro types + + DT_WARN = is_normal | is_warn, + DT_CHECK = is_normal | is_check, + DT_REQUIRE = is_normal | is_require, + + DT_WARN_FALSE = is_normal | is_false | is_warn, + DT_CHECK_FALSE = is_normal | is_false | is_check, + DT_REQUIRE_FALSE = is_normal | is_false | is_require, + + DT_WARN_THROWS = is_throws | is_warn, + DT_CHECK_THROWS = is_throws | is_check, + DT_REQUIRE_THROWS = is_throws | is_require, + + DT_WARN_THROWS_AS = is_throws_as | is_warn, + DT_CHECK_THROWS_AS = is_throws_as | is_check, + DT_REQUIRE_THROWS_AS = is_throws_as | is_require, + + DT_WARN_THROWS_WITH = is_throws_with | is_warn, + DT_CHECK_THROWS_WITH = is_throws_with | is_check, + DT_REQUIRE_THROWS_WITH = is_throws_with | is_require, + + DT_WARN_THROWS_WITH_AS = is_throws_with | is_throws_as | is_warn, + DT_CHECK_THROWS_WITH_AS = is_throws_with | is_throws_as | is_check, + DT_REQUIRE_THROWS_WITH_AS = is_throws_with | is_throws_as | is_require, + + DT_WARN_NOTHROW = is_nothrow | is_warn, + DT_CHECK_NOTHROW = is_nothrow | is_check, + DT_REQUIRE_NOTHROW = is_nothrow | is_require, + + DT_WARN_EQ = is_normal | is_eq | is_warn, + DT_CHECK_EQ = is_normal | is_eq | is_check, + DT_REQUIRE_EQ = is_normal | is_eq | is_require, + + DT_WARN_NE = is_normal | is_ne | is_warn, + DT_CHECK_NE = is_normal | is_ne | is_check, + DT_REQUIRE_NE = is_normal | is_ne | is_require, + + DT_WARN_GT = is_normal | is_gt | is_warn, + DT_CHECK_GT = is_normal | is_gt | is_check, + DT_REQUIRE_GT = is_normal | is_gt | is_require, + + DT_WARN_LT = is_normal | is_lt | is_warn, + DT_CHECK_LT = is_normal | is_lt | is_check, + DT_REQUIRE_LT = is_normal | is_lt | is_require, + + DT_WARN_GE = is_normal | is_ge | is_warn, + DT_CHECK_GE = is_normal | is_ge | is_check, + DT_REQUIRE_GE = is_normal | is_ge | is_require, + + DT_WARN_LE = is_normal | is_le | is_warn, + DT_CHECK_LE = is_normal | is_le | is_check, + DT_REQUIRE_LE = is_normal | is_le | is_require, + + DT_WARN_UNARY = is_normal | is_unary | is_warn, + DT_CHECK_UNARY = is_normal | is_unary | is_check, + DT_REQUIRE_UNARY = is_normal | is_unary | is_require, + + DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn, + DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check, + DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require, +}; +} // namespace assertType + +DOCTEST_INTERFACE const char *assertString(assertType::Enum at); +DOCTEST_INTERFACE const char *failureString(assertType::Enum at); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_TYPE +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_DATA +#define DOCTEST_PARTS_PUBLIC_ASSERT_DATA + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE TestCaseData; + +struct DOCTEST_INTERFACE AssertData { + // common - for all asserts + const TestCaseData *m_test_case; + assertType::Enum m_at; + const char *m_file; + int m_line; + const char *m_expr; + bool m_failed; + + // exception-related - for all asserts + bool m_threw; + String m_exception; + + // for normal asserts + String m_decomp; + + // for specific exception-related asserts + bool m_threw_as; + const char *m_exception_type; + + class DOCTEST_INTERFACE StringContains { + private: + Contains content; + bool isContains; + + public: + StringContains(const String &str) + : content(str), isContains(false) {} + + StringContains(Contains cntn) + : content(static_cast(cntn)), isContains(true) {} + + bool check(const String &str) { + return isContains ? (content == str) : (content.string == str); + } + + operator const String &() const { + return content.string; + } + + const char *c_str() const { + return content.string.c_str(); + } + } m_exception_string; + + AssertData( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const StringContains &exception_string + ); +}; + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_DATA +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_COMPARATOR +#define DOCTEST_PARTS_PUBLIC_ASSERT_COMPARATOR + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +// https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389 +DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +// clang-format off +template struct decay_array { using type = T; }; +template struct decay_array { using type = T *; }; +template struct decay_array { using type = T *; }; + +template struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 1; }; +template<> struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 0; }; +template<> struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 0; }; + +template struct can_use_op : public not_char_pointer::type> {}; +// clang-format on +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE bool +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +// clang-format off +#define DOCTEST_COMPARISON_RETURN_TYPE typename types::enable_if::value || can_use_op::value, bool>::type +inline bool eq(const char *lhs, const char *rhs) { return String(lhs) == String(rhs); } +inline bool ne(const char *lhs, const char *rhs) { return String(lhs) != String(rhs); } +inline bool lt(const char *lhs, const char *rhs) { return String(lhs) < String(rhs); } +inline bool gt(const char *lhs, const char *rhs) { return String(lhs) > String(rhs); } +inline bool le(const char *lhs, const char *rhs) { return String(lhs) <= String(rhs); } +inline bool ge(const char *lhs, const char *rhs) { return String(lhs) >= String(rhs); } +// clang-format on +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + DOCTEST_COMPARISON_RETURN_TYPE name(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { \ + return lhs op rhs; \ + } + +DOCTEST_RELATIONAL_OP(eq, ==) +DOCTEST_RELATIONAL_OP(ne, !=) +DOCTEST_RELATIONAL_OP(lt, <) +DOCTEST_RELATIONAL_OP(gt, >) +DOCTEST_RELATIONAL_OP(le, <=) +DOCTEST_RELATIONAL_OP(ge, >=) + +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) l == r +#define DOCTEST_CMP_NE(l, r) l != r +#define DOCTEST_CMP_GT(l, r) l > r +#define DOCTEST_CMP_LT(l, r) l < r +#define DOCTEST_CMP_GE(l, r) l >= r +#define DOCTEST_CMP_LE(l, r) l <= r +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) eq(l, r) +#define DOCTEST_CMP_NE(l, r) ne(l, r) +#define DOCTEST_CMP_GT(l, r) gt(l, r) +#define DOCTEST_CMP_LT(l, r) lt(l, r) +#define DOCTEST_CMP_GE(l, r) ge(l, r) +#define DOCTEST_CMP_LE(l, r) le(l, r) +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +namespace binaryAssertComparison { +enum Enum { eq = 0, ne, gt, lt, ge, le }; +} // namespace binaryAssertComparison + +// clang-format off +template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L), const DOCTEST_REF_WRAP(R) ) const { return false; } }; + +#define DOCTEST_BINARY_RELATIONAL_OP(n, op) \ + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) const { return op(lhs, rhs); } }; +// clang-format on + +DOCTEST_BINARY_RELATIONAL_OP(0, doctest::detail::eq) +DOCTEST_BINARY_RELATIONAL_OP(1, doctest::detail::ne) +DOCTEST_BINARY_RELATIONAL_OP(2, doctest::detail::gt) +DOCTEST_BINARY_RELATIONAL_OP(3, doctest::detail::lt) +DOCTEST_BINARY_RELATIONAL_OP(4, doctest::detail::ge) +DOCTEST_BINARY_RELATIONAL_OP(5, doctest::detail::le) + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_COMPARATOR +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_RESULT +#define DOCTEST_PARTS_PUBLIC_ASSERT_RESULT + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// more checks could be added - like in Catch: +// https://github.com/catchorg/Catch2/pull/1480/files +// https://github.com/catchorg/Catch2/pull/1481/files +#define DOCTEST_FORBIT_EXPRESSION(rt, op) \ + template \ + rt &operator op(const R &) { \ + static_assert(deferred_false::value, "Expression Too Complex Please Rewrite As Binary Comparison!"); \ + return *this; \ + } + +struct DOCTEST_INTERFACE Result { // NOLINT(*-member-init) + bool m_passed; + String m_decomp; + + Result() = default; // TODO: Why do we need this? (To remove NOLINT) + Result(bool passed, const String &decomposition = String()); + + // forbidding some expressions based on this table: + // https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Result, &) + DOCTEST_FORBIT_EXPRESSION(Result, ^) + DOCTEST_FORBIT_EXPRESSION(Result, |) + DOCTEST_FORBIT_EXPRESSION(Result, &&) + DOCTEST_FORBIT_EXPRESSION(Result, ||) + DOCTEST_FORBIT_EXPRESSION(Result, ==) + DOCTEST_FORBIT_EXPRESSION(Result, !=) + DOCTEST_FORBIT_EXPRESSION(Result, <) + DOCTEST_FORBIT_EXPRESSION(Result, >) + DOCTEST_FORBIT_EXPRESSION(Result, <=) + DOCTEST_FORBIT_EXPRESSION(Result, >=) + DOCTEST_FORBIT_EXPRESSION(Result, =) + DOCTEST_FORBIT_EXPRESSION(Result, +=) + DOCTEST_FORBIT_EXPRESSION(Result, -=) + DOCTEST_FORBIT_EXPRESSION(Result, *=) + DOCTEST_FORBIT_EXPRESSION(Result, /=) + DOCTEST_FORBIT_EXPRESSION(Result, %=) + DOCTEST_FORBIT_EXPRESSION(Result, <<=) + DOCTEST_FORBIT_EXPRESSION(Result, >>=) + DOCTEST_FORBIT_EXPRESSION(Result, &=) + DOCTEST_FORBIT_EXPRESSION(Result, ^=) + DOCTEST_FORBIT_EXPRESSION(Result, |=) +}; + +struct DOCTEST_INTERFACE ResultBuilder : public AssertData { + ResultBuilder( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type = "", + const String &exception_string = "" + ); + + ResultBuilder( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const Contains &exception_string + ); + + void setResult(const Result &res); + + template + DOCTEST_NOINLINE bool binary_assert(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { + m_failed = !RelationalComparator()(lhs, rhs); + if (m_failed || getContextOptions()->success) { + m_decomp = stringifyBinaryExpr(lhs, ", ", rhs); + } + return !m_failed; + } + + template + DOCTEST_NOINLINE bool unary_assert(const DOCTEST_REF_WRAP(L) val) { + m_failed = !val; + + if (m_at & assertType::is_false) { + m_failed = !m_failed; + } + + if (m_failed || getContextOptions()->success) { + m_decomp = (DOCTEST_STRINGIFY(val)); + } + + return !m_failed; + } + + void translateException(); + + bool log(); + void react() const; +}; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_RESULT +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_EXPRESSION +#define DOCTEST_PARTS_PUBLIC_ASSERT_EXPRESSION + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-comparison") +#endif + +// This will check if there is any way it could find a operator like member or friend and uses it. +// If not it doesn't find the operator or if the operator at global scope is defined after +// this template, the template won't be instantiated due to SFINAE. Once the template is not +// instantiated it can look for global operator using normal conversions. +#ifdef __NVCC__ +#define SFINAE_OP(ret, op) ret +#else +#define SFINAE_OP(ret, op) decltype((void)(doctest::detail::declval() op doctest::detail::declval()), ret{}) +#endif + +#define DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(op, op_str, op_macro) \ + /* NOLINTBEGIN(cppcoreguidelines-missing-std-forward) */ \ + template \ + DOCTEST_NOINLINE SFINAE_OP(Result, op) operator op(R &&rhs) { \ + bool res = op_macro(doctest::detail::forward(lhs), doctest::detail::forward(rhs)); \ + if (m_at & assertType::is_false) \ + res = !res; \ + if (!res || doctest::getContextOptions()->success) \ + return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ + return Result(res); \ + } \ + /* NOLINTEND(cppcoreguidelines-missing-std-forward) */ + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare") +// DOCTEST_CLANG_SUPPRESS_WARNING("-Wdouble-promotion") +// DOCTEST_CLANG_SUPPRESS_WARNING("-Wconversion") +// DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare") +// DOCTEST_GCC_SUPPRESS_WARNING("-Wdouble-promotion") +// DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") +// DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +// https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389 +DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch +// DOCTEST_MSVC_SUPPRESS_WARNING(4805) // 'operation' : unsafe mix of type 'type' and type 'type' in +// operation + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +template +struct Expression_lhs { + L lhs; + assertType::Enum m_at; + + // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) + explicit Expression_lhs(L &&in, assertType::Enum at) + : lhs(static_cast(in)), m_at(at) {} + + DOCTEST_NOINLINE operator Result() { + // this is needed only for MSVC 2015 + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4800) // 'int': forcing value to bool + bool res = static_cast(lhs); // NOLINT(bugprone-non-zero-enum-to-bool-conversion) + DOCTEST_MSVC_SUPPRESS_WARNING_POP + if (m_at & assertType::is_false) { + res = !res; + } + + if (!res || getContextOptions()->success) { + return {res, (DOCTEST_STRINGIFY(lhs))}; + } + return {res}; + } + + /* This is required for user-defined conversions from Expression_lhs to L */ + operator L() const { + return lhs; + } + + // clang-format off + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(==, " == ", DOCTEST_CMP_EQ) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(!=, " != ", DOCTEST_CMP_NE) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>, " > ", DOCTEST_CMP_GT) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<, " < ", DOCTEST_CMP_LT) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>=, " >= ", DOCTEST_CMP_GE) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<=, " <= ", DOCTEST_CMP_LE) + // clang-format on + + // forbidding some expressions based on this table: + // https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &&) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ||) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, =) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, +=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, -=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, *=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, /=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, %=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |=) + // these 2 are unfortunate because they should be allowed - they have higher precedence over the + // comparisons, but the ExpressionDecomposer class uses the left shift operator to capture the + // left operand of the binary expression... + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>) +}; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_POP +#endif + +struct DOCTEST_INTERFACE ExpressionDecomposer { + assertType::Enum m_at; + + ExpressionDecomposer(assertType::Enum at); + + // The right operator for capturing expressions is "<=" instead of "<<" (based on the operator + // precedence table) but then there will be warnings from GCC about "-Wparentheses" and since + // "_Pragma()" is problematic this will stay for now... + // https://github.com/catchorg/Catch2/issues/870 + // https://github.com/catchorg/Catch2/issues/565 + template + Expression_lhs + operator<<(const L &&operand) { // bitfields bind to universal ref but not const rvalue ref + return Expression_lhs(static_cast(operand), m_at); + } + + template < + typename L, + typename types::enable_if::value, void>::type * = nullptr> + Expression_lhs operator<<(const L &operand) { + return Expression_lhs(operand, m_at); + } +}; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_EXPRESSION +#ifndef DOCTEST_PARTS_PUBLIC_COLOR +#define DOCTEST_PARTS_PUBLIC_COLOR + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace Color { +enum Enum { // NOLINT(cert-int09-c, readability-enum-initial-value) + None = 0, + White, + Red, + Green, + Blue, + Cyan, + Yellow, + Grey, + + Bright = 0x10, + + BrightRed = Bright | Red, + BrightGreen = Bright | Green, + LightGrey = Bright | Grey, + BrightWhite = Bright | White +}; + +DOCTEST_INTERFACE std::ostream &operator<<(std::ostream &s, Color::Enum code); +} // namespace Color +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_COLOR +#ifndef DOCTEST_PARTS_PUBLIC_SUBCASE +#define DOCTEST_PARTS_PUBLIC_SUBCASE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE SubcaseSignature { + String m_name; + const char *m_file; + int m_line; + + bool operator==(const SubcaseSignature &other) const; + bool operator<(const SubcaseSignature &other) const; +}; + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { +struct DOCTEST_INTERFACE Subcase { + SubcaseSignature m_signature; + bool m_entered = false; + + Subcase(const String &name, const char *file, int line); + Subcase(const Subcase &) = delete; + Subcase(Subcase &&) = delete; + Subcase &operator=(const Subcase &) = delete; + Subcase &operator=(Subcase &&) = delete; + ~Subcase(); + + operator bool() const; + +private: + bool checkFilters(); +}; +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_SUBCASE +#ifndef DOCTEST_PARTS_PUBLIC_TEST_SUITE +#define DOCTEST_PARTS_PUBLIC_TEST_SUITE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +struct DOCTEST_INTERFACE TestSuite { + const char *m_test_suite = nullptr; + const char *m_description = nullptr; + bool m_skip = false; + bool m_no_breaks = false; + bool m_no_output = false; + bool m_may_fail = false; + bool m_should_fail = false; + int m_expected_failures = 0; + double m_timeout = 0; + + TestSuite &operator*(const char *in) noexcept; + + template + TestSuite &operator*(const T &in) noexcept { + in.fill(*this); + return *this; + } +}; + +// forward declarations of functions used by the macros +DOCTEST_INTERFACE int setTestSuite(const TestSuite &ts) noexcept; + +} // namespace detail + +} // namespace doctest + +// in a separate namespace outside of doctest because the DOCTEST_TEST_SUITE macro +// introduces an anonymous namespace in which getCurrentTestSuite gets overridden +namespace doctest_detail_test_suite_ns { +DOCTEST_INTERFACE doctest::detail::TestSuite &getCurrentTestSuite() noexcept; + +// this is here to clear the 'current test suite' for the current translation unit - at the top +DOCTEST_GLOBAL_NO_WARNINGS(/* NOLINT(cert-err58-cpp) */ + DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), + doctest::detail::setTestSuite(doctest::detail::TestSuite() * "") +) + +} // namespace doctest_detail_test_suite_ns + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_TEST_SUITE +#ifndef DOCTEST_PARTS_PUBLIC_TEST_CASE +#define DOCTEST_PARTS_PUBLIC_TEST_CASE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE TestCaseData { + String m_file; // the file in which the test was registered (using String - see #350) + unsigned m_line; // the line where the test was registered + const char *m_name; // name of the test case + const char *m_test_suite; // the test suite in which the test was added + const char *m_description; + bool m_skip; + bool m_no_breaks; + bool m_no_output; + bool m_may_fail; + bool m_should_fail; + int m_expected_failures; + double m_timeout; +}; + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { + +using funcType = void (*)(); + +struct DOCTEST_INTERFACE TestCase : public TestCaseData { + funcType m_test; // a function pointer to the test case + + String m_type; // for templated test cases - gets appended to the real name + int m_template_id; // an ID used to distinguish between the different versions of a templated + // test case + String m_full_name; // contains the name (only for templated test cases!) + the template type + + TestCase( + funcType test, + const char *file, + unsigned line, + const TestSuite &test_suite, + const String &type = String(), + int template_id = -1 + ) noexcept; + + TestCase(const TestCase &other) noexcept; + TestCase(TestCase &&) = delete; + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + TestCase &operator=(const TestCase &other) noexcept; + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase &operator=(TestCase &&) = delete; + + TestCase &operator*(const char *in) noexcept; + + template + TestCase &operator*(const T &in) noexcept { + in.fill(*this); + return *this; + } + + bool operator<(const TestCase &other) const noexcept; + + ~TestCase() = default; +}; + +// forward declarations of functions used by the macros +DOCTEST_INTERFACE int regTest(const TestCase &tc) noexcept; + +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_TEST_CASE +#ifndef DOCTEST_PARTS_PUBLIC_DECORATORS +#define DOCTEST_PARTS_PUBLIC_DECORATORS + + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +#define DOCTEST_DEFINE_DECORATOR(name, type, def) \ + struct name { \ + type data; \ + name(type in = def) noexcept \ + : data(in) {} \ + void fill(detail::TestCase &state) const noexcept { \ + state.DOCTEST_CAT(m_, name) = data; \ + } \ + void fill(detail::TestSuite &state) const noexcept { \ + state.DOCTEST_CAT(m_, name) = data; \ + } \ + } + +DOCTEST_DEFINE_DECORATOR(test_suite, const char *, ""); +DOCTEST_DEFINE_DECORATOR(description, const char *, ""); +DOCTEST_DEFINE_DECORATOR(skip, bool, true); +DOCTEST_DEFINE_DECORATOR(no_breaks, bool, true); +DOCTEST_DEFINE_DECORATOR(no_output, bool, true); +DOCTEST_DEFINE_DECORATOR(timeout, double, 0); +DOCTEST_DEFINE_DECORATOR(may_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(should_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(expected_failures, int, 0); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +#endif // DOCTEST_PARTS_PUBLIC_DECORATORS +#ifndef DOCTEST_PARTS_PUBLIC_EXCEPTION_TRANSLATOR +#define DOCTEST_PARTS_PUBLIC_EXCEPTION_TRANSLATOR + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +#ifndef DOCTEST_CONFIG_DISABLE + +struct DOCTEST_INTERFACE IExceptionTranslator { + DOCTEST_DECLARE_INTERFACE(IExceptionTranslator) + virtual bool translate(String &) const = 0; +}; + +template +class ExceptionTranslator : public IExceptionTranslator { +public: + explicit ExceptionTranslator(String (*translateFunction)(T)) noexcept + : m_translateFunction(translateFunction) {} + + bool translate(String &res) const override { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { + throw; + } catch (const T &ex) { + res = m_translateFunction(ex); + return true; + } catch (...) {} // NOLINT(bugprone-empty-catch) +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + static_cast(res); // to silence -Wunused-parameter + return false; + } + +private: + String (*m_translateFunction)(T); +}; + +DOCTEST_INTERFACE void registerExceptionTranslatorImpl(const IExceptionTranslator *et) noexcept; + +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace detail + +#ifndef DOCTEST_CONFIG_DISABLE + +template +int registerExceptionTranslator(String (*translateFunction)(T)) noexcept { + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") + static detail::ExceptionTranslator exceptionTranslator(translateFunction); + DOCTEST_CLANG_SUPPRESS_WARNING_POP + detail::registerExceptionTranslatorImpl(&exceptionTranslator); + return 0; +} + +#else // DOCTEST_CONFIG_DISABLE + +template +int registerExceptionTranslator(String (*)(T)) noexcept { + return 0; +} + +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_EXCEPTION_TRANSLATOR +#ifndef DOCTEST_PARTS_PUBLIC_CONTEXT_SCOPE +#define DOCTEST_PARTS_PUBLIC_CONTEXT_SCOPE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE IContextScope { + DOCTEST_DECLARE_INTERFACE(IContextScope) + virtual void stringify(std::ostream *) const = 0; +}; + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { + +// ContextScope base class used to allow implementing methods of ContextScope +// that don't depend on the template parameter in doctest.cpp. +struct DOCTEST_INTERFACE ContextScopeBase : public IContextScope { + ContextScopeBase(const ContextScopeBase &) = delete; + + ContextScopeBase &operator=(const ContextScopeBase &) = delete; + ContextScopeBase &operator=(ContextScopeBase &&) = delete; + + ~ContextScopeBase() override = default; + +protected: + ContextScopeBase(); + ContextScopeBase(ContextScopeBase &&other) noexcept; + + void destroy(); + bool need_to_destroy{true}; +}; + +template +class ContextScope : public ContextScopeBase { + L lambda_; + +public: + explicit ContextScope(const L &lambda) + : lambda_(lambda) {} + + // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) + explicit ContextScope(L &&lambda) + : lambda_(static_cast(lambda)) {} + + ContextScope(const ContextScope &) = delete; + ContextScope(ContextScope &&) noexcept = default; + + ContextScope &operator=(const ContextScope &) = delete; + ContextScope &operator=(ContextScope &&) = delete; + + void stringify(std::ostream *s) const override { + lambda_(s); + } + + ~ContextScope() override { + if (need_to_destroy) { + destroy(); + } + } +}; + +template +ContextScope MakeContextScope(const L &lambda) { + return ContextScope(lambda); +} + +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_CONTEXT_SCOPE +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_MESSAGE +#define DOCTEST_PARTS_PUBLIC_ASSERT_MESSAGE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE MessageData { + String m_string; + const char *m_file; + int m_line; + assertType::Enum m_severity; +}; + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { + +struct DOCTEST_INTERFACE MessageBuilder : public MessageData { + std::ostream *m_stream; + bool logged = false; + + MessageBuilder(const char *file, int line, assertType::Enum severity); + + MessageBuilder(const MessageBuilder &) = delete; + MessageBuilder(MessageBuilder &&) = delete; + + MessageBuilder &operator=(const MessageBuilder &) = delete; + MessageBuilder &operator=(MessageBuilder &&) = delete; + + ~MessageBuilder() noexcept(false); + + // the preferred way of chaining parameters for stringification + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866) + template + MessageBuilder &operator,(const T &in) { + *m_stream << (DOCTEST_STRINGIFY(in)); + return *this; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + // kept here just for backwards-compatibility - the comma operator should be preferred now + template + MessageBuilder &operator<<(const T &in) { + return this->operator,(in); + } + + // the `,` operator has the lowest operator precedence - if `<<` is used by the user then + // the `,` operator will be called last which is not what we want and thus the `*` operator + // is used first (has higher operator precedence compared to `<<`) so that we guarantee that + // an operator of the MessageBuilder class is called first before the rest of the parameters + template + MessageBuilder &operator*(const T &in) { + return this->operator,(in); + } + + bool log(); + void react(); +}; + +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_MESSAGE +#ifndef DOCTEST_PARTS_PUBLIC_PATH +#define DOCTEST_PARTS_PUBLIC_PATH + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +DOCTEST_INTERFACE const char *skipPathFromFilename(const char *file); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_PATH +#ifndef DOCTEST_PARTS_PUBLIC_EXCEPTIONS +#define DOCTEST_PARTS_PUBLIC_EXCEPTIONS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +struct DOCTEST_INTERFACE TestFailureException {}; + +DOCTEST_INTERFACE bool checkIfShouldThrow(assertType::Enum at); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +DOCTEST_NORETURN +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +DOCTEST_INTERFACE void throwException(); + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_EXCEPTIONS +#ifndef DOCTEST_PARTS_PUBLIC_CONTEXT +#define DOCTEST_PARTS_PUBLIC_CONTEXT + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +DOCTEST_INTERFACE extern bool is_running_in_test; + +namespace detail { +using assert_handler = void (*)(const AssertData &); +struct ContextState; +} // namespace detail + +class DOCTEST_INTERFACE Context { + detail::ContextState *p; + + void parseArgs(int argc, const char *const *argv, bool withDefaults = false); + +public: + explicit Context(int argc = 0, const char *const *argv = nullptr); + + Context(const Context &) = delete; + Context(Context &&) = delete; + + Context &operator=(const Context &) = delete; + Context &operator=(Context &&) = delete; + + ~Context(); // NOLINT(performance-trivially-destructible) + + void applyCommandLine(int argc, const char *const *argv); + + void addFilter(const char *filter, const char *value); + void clearFilters(); + void setOption(const char *option, bool value); + void setOption(const char *option, int value); + void setOption(const char *option, const char *value); + + bool shouldExit(); + + void setAsDefaultForAssertsOutOfTestCases(); + + void setAssertHandler(detail::assert_handler ah); + + void setCout(std::ostream *out); + + int run(); +}; +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_CONTEXT +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_HANDLER +#define DOCTEST_PARTS_PUBLIC_ASSERT_HANDLER + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +DOCTEST_INTERFACE void failed_out_of_a_testing_context(const AssertData &ad); + +DOCTEST_INTERFACE bool +decomp_assert(assertType::Enum at, const char *file, int line, const char *expr, const Result &result); + +#define DOCTEST_ASSERT_OUT_OF_TESTS(decomp) \ + do { /* NOLINT(cppcoreguidelines-avoid-do-while) */ \ + if (!is_running_in_test) { \ + if (failed) { \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + rb.m_decomp = decomp; \ + failed_out_of_a_testing_context(rb); \ + if (isDebuggerActive() && !getContextOptions()->no_breaks) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if (checkIfShouldThrow(at)) \ + throwException(); \ + } \ + return !failed; \ + } \ + } while (false) + +#define DOCTEST_ASSERT_IN_TESTS(decomp) \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + if (rb.m_failed || getContextOptions()->success) \ + rb.m_decomp = decomp; \ + if (rb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if (rb.m_failed && checkIfShouldThrow(at)) \ + throwException() + +template +DOCTEST_NOINLINE bool binary_assert( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs +) { + const bool failed = !RelationalComparator()(lhs, rhs); + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + DOCTEST_ASSERT_IN_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + return !failed; +} + +template +DOCTEST_NOINLINE bool +unary_assert(assertType::Enum at, const char *file, int line, const char *expr, const DOCTEST_REF_WRAP(L) val) { + bool failed = !val; + + if (at & assertType::is_false) + failed = !failed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS((DOCTEST_STRINGIFY(val))); + DOCTEST_ASSERT_IN_TESTS((DOCTEST_STRINGIFY(val))); + return !failed; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_HANDLER +#ifndef DOCTEST_PARTS_PUBLIC_REPORTER +#define DOCTEST_PARTS_PUBLIC_REPORTER + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +namespace TestCaseFailureReason { +enum Enum { + None = 0, + AssertFailure = 1, // an assertion has failed in the test case + Exception = 2, // test case threw an exception + Crash = 4, // a crash... + TooManyFailedAsserts = 8, // the abort-after option + Timeout = 16, // see the timeout decorator + ShouldHaveFailedButDidnt = 32, // see the should_fail decorator + ShouldHaveFailedAndDid = 64, // see the should_fail decorator + DidntFailExactlyNumTimes = 128, // see the expected_failures decorator + FailedExactlyNumTimes = 256, // see the expected_failures decorator + CouldHaveFailedAndDid = 512 // see the may_fail decorator +}; +} // namespace TestCaseFailureReason + +struct DOCTEST_INTERFACE CurrentTestCaseStats { + int numAssertsCurrentTest; + int numAssertsFailedCurrentTest; + double seconds; + int failure_flags; // use TestCaseFailureReason::Enum + bool testCaseSuccess; +}; + +struct DOCTEST_INTERFACE TestCaseException { + String error_string; + bool is_crash; +}; + +struct DOCTEST_INTERFACE TestRunStats { + unsigned numTestCases; + unsigned numTestCasesPassingFilters; + unsigned numTestSuitesPassingFilters; + unsigned numTestCasesFailed; + int numAsserts; + int numAssertsFailed; +}; + +struct QueryData { + const TestRunStats *run_stats = nullptr; + const TestCaseData **data = nullptr; + unsigned num_data = 0; +}; + +struct DOCTEST_INTERFACE IReporter { + // The constructor has to accept "const ContextOptions&" as a single argument + // which has most of the options for the run + a pointer to the stdout stream + // Reporter(const ContextOptions& in) + + // called when a query should be reported (listing test cases, printing the version, etc.) + virtual void report_query(const QueryData &) = 0; + + // called when the whole test run starts + virtual void test_run_start() = 0; + // called when the whole test run ends (caching a pointer to the input doesn't make sense here) + virtual void test_run_end(const TestRunStats &) = 0; + + // called when a test case is started (safe to cache a pointer to the input) + virtual void test_case_start(const TestCaseData &) = 0; + // called when a test case is reentered because of unfinished subcases (safe to cache a pointer to the input) + virtual void test_case_reenter(const TestCaseData &) = 0; + // called when a test case has ended + virtual void test_case_end(const CurrentTestCaseStats &) = 0; + + // called when an exception is thrown from the test case (or it crashes) + virtual void test_case_exception(const TestCaseException &) = 0; + + // called whenever a subcase is entered (don't cache pointers to the input) + virtual void subcase_start(const SubcaseSignature &) = 0; + // called whenever a subcase is exited (don't cache pointers to the input) + virtual void subcase_end() = 0; + + // called for each assert (don't cache pointers to the input) + virtual void log_assert(const AssertData &) = 0; + // called for each message (don't cache pointers to the input) + virtual void log_message(const MessageData &) = 0; + + // called when a test case is skipped either because it doesn't pass the filters, + // has a skip decorator or isn't in the execution range (between first and last) + // (safe to cache a pointer to the input) + virtual void test_case_skipped(const TestCaseData &) = 0; + + DOCTEST_DECLARE_INTERFACE(IReporter) + + // can obtain all currently active contexts and stringify them if one wishes to do so + static int get_num_active_contexts(); + static const IContextScope *const *get_active_contexts(); + + // can iterate through contexts which have been stringified automatically + // in their destructors when an exception has been thrown + static int get_num_stringified_contexts(); + static const String *get_stringified_contexts(); +}; + +namespace detail { +using reporterCreatorFunc = IReporter *(*)(const ContextOptions &); + +DOCTEST_INTERFACE void +registerReporterImpl(const char *name, int prio, reporterCreatorFunc c, bool isReporter) noexcept; + +template +IReporter *reporterCreator(const ContextOptions &o) { + return new Reporter(o); +} +} // namespace detail + +template +int registerReporter(const char *name, int priority, bool isReporter) noexcept { + detail::registerReporterImpl(name, priority, detail::reporterCreator, isReporter); + return 0; +} +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_REPORTER +#ifndef DOCTEST_PARTS_PUBLIC_GENERATOR +#define DOCTEST_PARTS_PUBLIC_GENERATOR + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE +namespace doctest { +namespace detail { + +DOCTEST_INTERFACE size_t acquireGeneratorDecisionIndex(size_t count); + +template +T acquireGeneratorValue(T first, Rest... rest) { + const T values[] = {first, static_cast(rest)...}; + const size_t idx = acquireGeneratorDecisionIndex(1 + sizeof...(Rest)); + return values[idx]; +} + +} // namespace detail +} // namespace doctest +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_GENERATOR +#ifndef DOCTEST_PARTS_PUBLIC_MACROS +#define DOCTEST_PARTS_PUBLIC_MACROS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE +namespace doctest { +namespace detail { +template +int instantiationHelper(const T &) noexcept { + return 0; +} + +} // namespace detail +} // namespace doctest +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES +#define DOCTEST_FUNC_EMPTY [] { return false; }() +#else +#define DOCTEST_FUNC_EMPTY (void)0 +#endif + +// if registering is not disabled +#ifndef DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES +#define DOCTEST_FUNC_SCOPE_BEGIN [&] +#define DOCTEST_FUNC_SCOPE_END () +#define DOCTEST_FUNC_SCOPE_RET(v) return v +#else +#define DOCTEST_FUNC_SCOPE_BEGIN do /* NOLINT(cppcoreguidelines-avoid-do-while)*/ +#define DOCTEST_FUNC_SCOPE_END while (false) +#define DOCTEST_FUNC_SCOPE_RET(v) (void)0 +#endif + +// common code in asserts - for convenience +#define DOCTEST_ASSERT_LOG_REACT_RETURN(b) \ + if (b.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + b.react(); \ + DOCTEST_FUNC_SCOPE_RET(!b.m_failed) + +#ifdef DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) x; +#else // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) \ + try { \ + x; \ + } catch (...) { DOCTEST_RB.translateException(); } +#endif // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wuseless-cast") \ + static_cast(__VA_ARGS__); \ + DOCTEST_GCC_SUPPRESS_WARNING_POP +#else // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) __VA_ARGS__; +#endif // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS + +// registers the test by initializing a dummy var with a function +#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators) \ + global_prefix DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT */ \ + doctest::detail::regTest( \ + doctest::detail::TestCase(f, __FILE__, __LINE__, doctest_detail_test_suite_ns::getCurrentTestSuite()) * \ + decorators \ + ) \ + ) + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, decorators) \ + namespace { /* NOLINT */ \ + struct der : public base { \ + void f(); \ + }; \ + static DOCTEST_INLINE_NOINLINE void func() { \ + der v; \ + v.f(); \ + } \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, func, decorators) \ + } \ + DOCTEST_INLINE_NOINLINE void der::f() // NOLINT(misc-definitions-in-headers) + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators) \ + static void f(); \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators) \ + static void f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(f, proxy, decorators) \ + static doctest::detail::funcType proxy() { \ + return f; \ + } \ + DOCTEST_REGISTER_FUNCTION(inline, proxy(), decorators) \ + static void f() + +// for registering tests +#define DOCTEST_TEST_CASE(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) + +// for registering tests in classes - requires C++17 for inline variables! +#if DOCTEST_CPLUSPLUS >= 201703L +#define DOCTEST_TEST_CASE_CLASS(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), DOCTEST_ANONYMOUS(DOCTEST_ANON_PROXY_), decorators \ + ) +#else // DOCTEST_TEST_CASE_CLASS +#define DOCTEST_TEST_CASE_CLASS(...) TEST_CASES_CAN_BE_REGISTERED_IN_CLASSES_ONLY_IN_CPP17_MODE_OR_WITH_VS_2017_OR_NEWER +#endif // DOCTEST_TEST_CASE_CLASS + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(c, decorators) \ + DOCTEST_IMPLEMENT_FIXTURE( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), c, DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators \ + ) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_AS(str, ...) \ + namespace doctest { \ + template <> \ + inline String toString<__VA_ARGS__>() { \ + return str; \ + } \ + } \ + static_assert(true, "") + +#define DOCTEST_TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING_AS(#__VA_ARGS__, __VA_ARGS__) + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, iter, func) \ + template \ + static void func(); \ + namespace { /* NOLINT */ \ + template \ + struct iter; \ + template \ + struct iter> { \ + iter(const char *file, unsigned line, int index) noexcept { \ + doctest::detail::regTest( \ + doctest::detail::TestCase( \ + func, \ + file, \ + line, \ + doctest_detail_test_suite_ns::getCurrentTestSuite(), \ + doctest::toString(), \ + int(line) * 1000 + index \ + ) * \ + dec \ + ); \ + iter>(file, line, index + 1); \ + } \ + }; \ + template <> \ + struct iter> { \ + iter(const char *, unsigned, int) {} \ + }; \ + } \ + template \ + static void func() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(dec, T, id) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(id, ITERATOR), DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)) + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, anon, ...) \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_CAT(anon, DUMMY), /* NOLINT(cert-err58-cpp, fuchsia-statically-constructed-objects) */ \ + doctest::detail::instantiationHelper(DOCTEST_CAT(id, ITERATOR) < __VA_ARGS__ > (__FILE__, __LINE__, 0)) \ + ) + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), std::tuple<__VA_ARGS__>) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, anon, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(anon, ITERATOR), anon); \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(anon, anon, std::tuple<__VA_ARGS__>) \ + template \ + static void anon() + +#define DOCTEST_TEST_CASE_TEMPLATE(dec, T, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) + +// for subcases +#define DOCTEST_SUBCASE(name) \ + if (const doctest::detail::Subcase &DOCTEST_ANONYMOUS(DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED = \ + doctest::detail::Subcase(name, __FILE__, __LINE__)) + +// for generating value-parameterized test inputs +#define DOCTEST_GENERATE(...) doctest::detail::acquireGeneratorValue(__VA_ARGS__) + +// for grouping tests in test suites by using code blocks +#define DOCTEST_TEST_SUITE_IMPL(decorators, ns_name) \ + namespace ns_name { \ + namespace doctest_detail_test_suite_ns { \ + static DOCTEST_NOINLINE doctest::detail::TestSuite &getCurrentTestSuite() noexcept { \ + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4640) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmissing-field-initializers") \ + static doctest::detail::TestSuite data{}; \ + static bool inited = false; \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + if (!inited) { \ + data *decorators; \ + inited = true; \ + } \ + return data; \ + } \ + } \ + } \ + namespace ns_name + +#define DOCTEST_TEST_SUITE(decorators) DOCTEST_TEST_SUITE_IMPL(decorators, DOCTEST_ANONYMOUS(DOCTEST_ANON_SUITE_)) + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(decorators) \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * decorators) \ + ) \ + static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * "") \ + ) \ + using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int + +// for registering exception translators +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(translatorName, signature) \ + inline doctest::String translatorName(signature); \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerExceptionTranslator(translatorName) \ + ) \ + doctest::String translatorName(signature) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), signature) + +// for registering reporters +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerReporter(name, priority, true) \ + ) \ + static_assert(true, "") + +// for registering listeners +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerReporter(name, priority, false) \ + ) \ + static_assert(true, "") + +#define DOCTEST_INFO(...) \ + DOCTEST_INFO_IMPL(DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_MB_), DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_OTHER_), __VA_ARGS__) + +#define DOCTEST_INFO_IMPL(mb_name, s_name, ...) \ + auto DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_) = doctest::detail::MakeContextScope([&](std::ostream *s_name) { \ + doctest::detail::MessageBuilder mb_name(__FILE__, __LINE__, doctest::assertType::is_warn); \ + mb_name.m_stream = s_name; \ + mb_name *__VA_ARGS__; \ + }) + +#define DOCTEST_CAPTURE(x) DOCTEST_INFO(#x " := ", x) + +#define DOCTEST_ADD_AT_IMPL(type, file, line, mb, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::MessageBuilder mb(file, line, doctest::assertType::type); \ + mb *__VA_ARGS__; \ + if (mb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + mb.react(); \ + } \ + DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) \ + DOCTEST_ADD_AT_IMPL(is_warn, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) \ + DOCTEST_ADD_AT_IMPL(is_check, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) \ + DOCTEST_ADD_AT_IMPL(is_require, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) + +#define DOCTEST_MESSAGE(...) DOCTEST_ADD_MESSAGE_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL_CHECK(...) DOCTEST_ADD_FAIL_CHECK_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL(...) DOCTEST_ADD_FAIL_AT(__FILE__, __LINE__, __VA_ARGS__) + +#define DOCTEST_TO_LVALUE(...) __VA_ARGS__ // Not removed to keep backwards compatibility. + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_ASSERT_IMPLEMENT_2(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY( \ + DOCTEST_RB.setResult(doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) << __VA_ARGS__) \ + ) /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB) \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + DOCTEST_ASSERT_IMPLEMENT_2(assert_type, __VA_ARGS__); \ + } \ + DOCTEST_FUNC_SCOPE_END // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +#define DOCTEST_BINARY_ASSERT(assert_type, comp, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.binary_assert(__VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } \ + DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.unary_assert(__VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } \ + DOCTEST_FUNC_SCOPE_END + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +// necessary for _MESSAGE +#define DOCTEST_ASSERT_IMPLEMENT_2 DOCTEST_ASSERT_IMPLEMENT_1 + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::decomp_assert( \ + doctest::assertType::assert_type, \ + __FILE__, \ + __LINE__, \ + #__VA_ARGS__, \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) << __VA_ARGS__ \ + ) DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_BINARY_ASSERT(assert_type, comparison, ...) \ + doctest::detail::binary_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__ \ + ) + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + doctest::detail::unary_assert(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN, __VA_ARGS__) +#define DOCTEST_CHECK(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK, __VA_ARGS__) +#define DOCTEST_REQUIRE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE, __VA_ARGS__) +#define DOCTEST_WARN_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE_FALSE, __VA_ARGS__) + +// clang-format off +#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +// clang-format on + +#define DOCTEST_WARN_EQ(...) DOCTEST_BINARY_ASSERT(DT_WARN_EQ, eq, __VA_ARGS__) +#define DOCTEST_CHECK_EQ(...) DOCTEST_BINARY_ASSERT(DT_CHECK_EQ, eq, __VA_ARGS__) +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_EQ, eq, __VA_ARGS__) +#define DOCTEST_WARN_NE(...) DOCTEST_BINARY_ASSERT(DT_WARN_NE, ne, __VA_ARGS__) +#define DOCTEST_CHECK_NE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_NE, ne, __VA_ARGS__) +#define DOCTEST_REQUIRE_NE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_NE, ne, __VA_ARGS__) +#define DOCTEST_WARN_GT(...) DOCTEST_BINARY_ASSERT(DT_WARN_GT, gt, __VA_ARGS__) +#define DOCTEST_CHECK_GT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GT, gt, __VA_ARGS__) +#define DOCTEST_REQUIRE_GT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GT, gt, __VA_ARGS__) +#define DOCTEST_WARN_LT(...) DOCTEST_BINARY_ASSERT(DT_WARN_LT, lt, __VA_ARGS__) +#define DOCTEST_CHECK_LT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LT, lt, __VA_ARGS__) +#define DOCTEST_REQUIRE_LT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LT, lt, __VA_ARGS__) +#define DOCTEST_WARN_GE(...) DOCTEST_BINARY_ASSERT(DT_WARN_GE, ge, __VA_ARGS__) +#define DOCTEST_CHECK_GE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GE, ge, __VA_ARGS__) +#define DOCTEST_REQUIRE_GE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GE, ge, __VA_ARGS__) +#define DOCTEST_WARN_LE(...) DOCTEST_BINARY_ASSERT(DT_WARN_LE, le, __VA_ARGS__) +#define DOCTEST_CHECK_LE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LE, le, __VA_ARGS__) +#define DOCTEST_REQUIRE_LE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LE, le, __VA_ARGS__) + +#define DOCTEST_WARN_UNARY(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY, __VA_ARGS__) +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__) + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_ASSERT_THROWS_AS(expr, assert_type, message, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + if (!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #expr, #__VA_ARGS__, message \ + ); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch (const typename doctest::detail::types::remove_const< \ + typename doctest::detail::types::remove_reference<__VA_ARGS__>::type>::type &) { \ + DOCTEST_RB.translateException(); \ + DOCTEST_RB.m_threw_as = true; \ + } catch (...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { /* NOLINT(*-else-after-return) */ \ + DOCTEST_FUNC_SCOPE_RET(false); \ + } \ + } \ + DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ASSERT_THROWS_WITH(expr, expr_str, assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + if (!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB( \ + doctest::assertType::assert_type, __FILE__, __LINE__, expr_str, "", __VA_ARGS__ \ + ); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch (...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { /* NOLINT(*-else-after-return) */ \ + DOCTEST_FUNC_SCOPE_RET(false); \ + } \ + } \ + DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ASSERT_NOTHROW(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(__VA_ARGS__) \ + } catch (...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } \ + DOCTEST_FUNC_SCOPE_END + +// clang-format off +#define DOCTEST_WARN_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_WARN_THROWS, "") +#define DOCTEST_CHECK_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_CHECK_THROWS, "") +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_REQUIRE_THROWS, "") + +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_AS, "", __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_WARN_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_CHECK_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_REQUIRE_THROWS_WITH, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_WITH_AS, message, __VA_ARGS__) + +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_WARN_NOTHROW, __VA_ARGS__) +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_CHECK_NOTHROW, __VA_ARGS__) +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_REQUIRE_NOTHROW, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +// clang-format on + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// ================================================================================================= +// == WHAT FOLLOWS IS VERSIONS OF THE MACROS THAT DO NOT DO ANY REGISTERING! == +// == THIS CAN BE ENABLED BY DEFINING DOCTEST_CONFIG_DISABLE GLOBALLY! == +// ================================================================================================= +#else // DOCTEST_CONFIG_DISABLE + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, name) \ + namespace /* NOLINT */ { \ + template \ + struct der : public base { \ + void f(); \ + }; \ + } \ + template \ + inline void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, name) \ + template \ + static inline void f() + +// for registering tests +#define DOCTEST_TEST_CASE(name) DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests in classes +#define DOCTEST_TEST_CASE_CLASS(name) DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(x, name) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), x, DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_AS(str, ...) static_assert(true, "") +#define DOCTEST_TYPE_TO_STRING(...) static_assert(true, "") + +// for typed tests +#define DOCTEST_TEST_CASE_TEMPLATE(name, type, ...) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, type, id) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) static_assert(true, "") +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) static_assert(true, "") + +// for subcases +#define DOCTEST_SUBCASE(name) + +// for generating value-parameterized test inputs +#define DOCTEST_GENERATE_IMPL(first, ...) (first) +#define DOCTEST_GENERATE(...) DOCTEST_GENERATE_IMPL(__VA_ARGS__, DOCTEST_EMPTY) + +// for a testsuite block +#define DOCTEST_TEST_SUITE(name) namespace // NOLINT + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(name) static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + template \ + static inline doctest::String DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_)(signature) + +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) + +#define DOCTEST_INFO(...) (static_cast(0)) +#define DOCTEST_CAPTURE(x) (static_cast(0)) +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_MESSAGE(...) (static_cast(0)) +#define DOCTEST_FAIL_CHECK(...) (static_cast(0)) +#define DOCTEST_FAIL(...) (static_cast(0)) + +#if defined(DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED) && defined(DOCTEST_CONFIG_ASSERTS_RETURN_VALUES) + +#define DOCTEST_WARN(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#define DOCTEST_WARN_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_CHECK_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() + +namespace doctest { +namespace detail { +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + bool name(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { \ + return lhs op rhs; \ + } + +DOCTEST_RELATIONAL_OP(eq, ==) +DOCTEST_RELATIONAL_OP(ne, !=) +DOCTEST_RELATIONAL_OP(lt, <) +DOCTEST_RELATIONAL_OP(gt, >) +DOCTEST_RELATIONAL_OP(le, <=) +DOCTEST_RELATIONAL_OP(ge, >=) +} // namespace detail +} // namespace doctest + +#define DOCTEST_WARN_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_CHECK_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_WARN_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_CHECK_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_WARN_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_CHECK_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_WARN_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_CHECK_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_WARN_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_CHECK_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_WARN_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_CHECK_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_WARN_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_WARN_THROWS_WITH(expr, with, ...) \ + [] { \ + static_assert(false, "Exception translation is not available when doctest is disabled."); \ + return false; \ + }() +#define DOCTEST_CHECK_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) + +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) + +// clang-format off +#define DOCTEST_WARN_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_CHECK_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_REQUIRE_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_WARN_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_CHECK_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_WARN_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_CHECK_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_REQUIRE_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +// clang-format on + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#else // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#define DOCTEST_WARN(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_LE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_LE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_LE(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_WARN_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#endif // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#define DOCTEST_EXCEPTION_EMPTY_FUNC DOCTEST_FUNC_EMPTY +#else // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#define DOCTEST_EXCEPTION_EMPTY_FUNC \ + [] { \ + static_assert( \ + false, \ + "Exceptions are disabled! " \ + "Use DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS if you want " \ + "to compile with exceptions disabled." \ + ); \ + return false; \ + }() + +#undef DOCTEST_REQUIRE +#undef DOCTEST_REQUIRE_FALSE +#undef DOCTEST_REQUIRE_MESSAGE +#undef DOCTEST_REQUIRE_FALSE_MESSAGE +#undef DOCTEST_REQUIRE_EQ +#undef DOCTEST_REQUIRE_NE +#undef DOCTEST_REQUIRE_GT +#undef DOCTEST_REQUIRE_LT +#undef DOCTEST_REQUIRE_GE +#undef DOCTEST_REQUIRE_LE +#undef DOCTEST_REQUIRE_UNARY +#undef DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_REQUIRE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_FALSE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_EQ DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_GT DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_LT DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_GE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_LE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_UNARY DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_UNARY_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#define DOCTEST_WARN_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// KEPT FOR BACKWARDS COMPATIBILITY - FORWARDING TO THE RIGHT MACROS +#define DOCTEST_FAST_WARN_EQ DOCTEST_WARN_EQ +#define DOCTEST_FAST_CHECK_EQ DOCTEST_CHECK_EQ +#define DOCTEST_FAST_REQUIRE_EQ DOCTEST_REQUIRE_EQ +#define DOCTEST_FAST_WARN_NE DOCTEST_WARN_NE +#define DOCTEST_FAST_CHECK_NE DOCTEST_CHECK_NE +#define DOCTEST_FAST_REQUIRE_NE DOCTEST_REQUIRE_NE +#define DOCTEST_FAST_WARN_GT DOCTEST_WARN_GT +#define DOCTEST_FAST_CHECK_GT DOCTEST_CHECK_GT +#define DOCTEST_FAST_REQUIRE_GT DOCTEST_REQUIRE_GT +#define DOCTEST_FAST_WARN_LT DOCTEST_WARN_LT +#define DOCTEST_FAST_CHECK_LT DOCTEST_CHECK_LT +#define DOCTEST_FAST_REQUIRE_LT DOCTEST_REQUIRE_LT +#define DOCTEST_FAST_WARN_GE DOCTEST_WARN_GE +#define DOCTEST_FAST_CHECK_GE DOCTEST_CHECK_GE +#define DOCTEST_FAST_REQUIRE_GE DOCTEST_REQUIRE_GE +#define DOCTEST_FAST_WARN_LE DOCTEST_WARN_LE +#define DOCTEST_FAST_CHECK_LE DOCTEST_CHECK_LE +#define DOCTEST_FAST_REQUIRE_LE DOCTEST_REQUIRE_LE + +#define DOCTEST_FAST_WARN_UNARY DOCTEST_WARN_UNARY +#define DOCTEST_FAST_CHECK_UNARY DOCTEST_CHECK_UNARY +#define DOCTEST_FAST_REQUIRE_UNARY DOCTEST_REQUIRE_UNARY +#define DOCTEST_FAST_WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE +#define DOCTEST_FAST_CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE +#define DOCTEST_FAST_REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__) + +// BDD style macros +// clang-format off +#define DOCTEST_SCENARIO(name) DOCTEST_TEST_CASE(" Scenario: " name) +#define DOCTEST_SCENARIO_CLASS(name) DOCTEST_TEST_CASE_CLASS(" Scenario: " name) +#define DOCTEST_SCENARIO_METHOD(x, name) DOCTEST_TEST_CASE_FIXTURE(x, " Scenario: " name) +#define DOCTEST_SCENARIO_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(" Scenario: " name, T, __VA_ARGS__) +#define DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(" Scenario: " name, T, id) + +#define DOCTEST_GIVEN(name) DOCTEST_SUBCASE(" Given: " name) +#define DOCTEST_AND_GIVEN(name) DOCTEST_SUBCASE(" And: " name) +#define DOCTEST_WHEN(name) DOCTEST_SUBCASE(" When: " name) +#define DOCTEST_AND_WHEN(name) DOCTEST_SUBCASE(" And: " name) +#define DOCTEST_THEN(name) DOCTEST_SUBCASE(" Then: " name) +#define DOCTEST_AND_THEN(name) DOCTEST_SUBCASE(" And: " name) +// clang-format on + +// == SHORT VERSIONS OF THE MACROS +#ifndef DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +#define TEST_CASE(name) DOCTEST_TEST_CASE(name) +#define TEST_CASE_CLASS(name) DOCTEST_TEST_CASE_CLASS(name) +#define TEST_CASE_FIXTURE(x, name) DOCTEST_TEST_CASE_FIXTURE(x, name) +#define TYPE_TO_STRING_AS(str, ...) DOCTEST_TYPE_TO_STRING_AS(str, __VA_ARGS__) +#define TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING(__VA_ARGS__) +#define TEST_CASE_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(name, T, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, T, id) +#define TEST_CASE_TEMPLATE_INVOKE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_APPLY(id, ...) DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, __VA_ARGS__) +#define SUBCASE(name) DOCTEST_SUBCASE(name) +#define GENERATE(...) DOCTEST_GENERATE(__VA_ARGS__) +#define TEST_SUITE(decorators) DOCTEST_TEST_SUITE(decorators) +#define TEST_SUITE_BEGIN(name) DOCTEST_TEST_SUITE_BEGIN(name) +#define TEST_SUITE_END DOCTEST_TEST_SUITE_END +#define REGISTER_EXCEPTION_TRANSLATOR(signature) DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) +#define REGISTER_REPORTER(name, priority, reporter) DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define REGISTER_LISTENER(name, priority, reporter) DOCTEST_REGISTER_LISTENER(name, priority, reporter) +#define INFO(...) DOCTEST_INFO(__VA_ARGS__) +#define CAPTURE(x) DOCTEST_CAPTURE(x) +#define ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_MESSAGE_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_FAIL_CHECK_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_FAIL_AT(file, line, __VA_ARGS__) +#define MESSAGE(...) DOCTEST_MESSAGE(__VA_ARGS__) +#define FAIL_CHECK(...) DOCTEST_FAIL_CHECK(__VA_ARGS__) +#define FAIL(...) DOCTEST_FAIL(__VA_ARGS__) +#define TO_LVALUE(...) DOCTEST_TO_LVALUE(__VA_ARGS__) + +#define WARN(...) DOCTEST_WARN(__VA_ARGS__) +#define WARN_FALSE(...) DOCTEST_WARN_FALSE(__VA_ARGS__) +#define WARN_THROWS(...) DOCTEST_WARN_THROWS(__VA_ARGS__) +#define WARN_THROWS_AS(expr, ...) DOCTEST_WARN_THROWS_AS(expr, __VA_ARGS__) +#define WARN_THROWS_WITH(expr, ...) DOCTEST_WARN_THROWS_WITH(expr, __VA_ARGS__) +#define WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_WARN_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define WARN_NOTHROW(...) DOCTEST_WARN_NOTHROW(__VA_ARGS__) +#define CHECK(...) DOCTEST_CHECK(__VA_ARGS__) +#define CHECK_FALSE(...) DOCTEST_CHECK_FALSE(__VA_ARGS__) +#define CHECK_THROWS(...) DOCTEST_CHECK_THROWS(__VA_ARGS__) +#define CHECK_THROWS_AS(expr, ...) DOCTEST_CHECK_THROWS_AS(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH(expr, ...) DOCTEST_CHECK_THROWS_WITH(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define CHECK_NOTHROW(...) DOCTEST_CHECK_NOTHROW(__VA_ARGS__) +#define REQUIRE(...) DOCTEST_REQUIRE(__VA_ARGS__) +#define REQUIRE_FALSE(...) DOCTEST_REQUIRE_FALSE(__VA_ARGS__) +#define REQUIRE_THROWS(...) DOCTEST_REQUIRE_THROWS(__VA_ARGS__) +#define REQUIRE_THROWS_AS(expr, ...) DOCTEST_REQUIRE_THROWS_AS(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH(expr, ...) DOCTEST_REQUIRE_THROWS_WITH(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define REQUIRE_NOTHROW(...) DOCTEST_REQUIRE_NOTHROW(__VA_ARGS__) + +// clang-format off +#define WARN_MESSAGE(cond, ...) DOCTEST_WARN_MESSAGE(cond, __VA_ARGS__) +#define WARN_FALSE_MESSAGE(cond, ...) DOCTEST_WARN_FALSE_MESSAGE(cond, __VA_ARGS__) +#define WARN_THROWS_MESSAGE(expr, ...) DOCTEST_WARN_THROWS_MESSAGE(expr, __VA_ARGS__) +#define WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_WARN_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define CHECK_MESSAGE(cond, ...) DOCTEST_CHECK_MESSAGE(cond, __VA_ARGS__) +#define CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_CHECK_FALSE_MESSAGE(cond, __VA_ARGS__) +#define CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_CHECK_THROWS_MESSAGE(expr, __VA_ARGS__) +#define CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_CHECK_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_MESSAGE(cond, ...) DOCTEST_REQUIRE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_REQUIRE_FALSE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_REQUIRE_THROWS_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, __VA_ARGS__) +// clang-format on + +#define SCENARIO(name) DOCTEST_SCENARIO(name) +#define SCENARIO_METHOD(x, name) DOCTEST_SCENARIO_METHOD(x, name) +#define SCENARIO_CLASS(name) DOCTEST_SCENARIO_CLASS(name) +#define SCENARIO_TEMPLATE(name, T, ...) DOCTEST_SCENARIO_TEMPLATE(name, T, __VA_ARGS__) +#define SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) +#define GIVEN(name) DOCTEST_GIVEN(name) +#define AND_GIVEN(name) DOCTEST_AND_GIVEN(name) +#define WHEN(name) DOCTEST_WHEN(name) +#define AND_WHEN(name) DOCTEST_AND_WHEN(name) +#define THEN(name) DOCTEST_THEN(name) +#define AND_THEN(name) DOCTEST_AND_THEN(name) + +#define WARN_EQ(...) DOCTEST_WARN_EQ(__VA_ARGS__) +#define CHECK_EQ(...) DOCTEST_CHECK_EQ(__VA_ARGS__) +#define REQUIRE_EQ(...) DOCTEST_REQUIRE_EQ(__VA_ARGS__) +#define WARN_NE(...) DOCTEST_WARN_NE(__VA_ARGS__) +#define CHECK_NE(...) DOCTEST_CHECK_NE(__VA_ARGS__) +#define REQUIRE_NE(...) DOCTEST_REQUIRE_NE(__VA_ARGS__) +#define WARN_GT(...) DOCTEST_WARN_GT(__VA_ARGS__) +#define CHECK_GT(...) DOCTEST_CHECK_GT(__VA_ARGS__) +#define REQUIRE_GT(...) DOCTEST_REQUIRE_GT(__VA_ARGS__) +#define WARN_LT(...) DOCTEST_WARN_LT(__VA_ARGS__) +#define CHECK_LT(...) DOCTEST_CHECK_LT(__VA_ARGS__) +#define REQUIRE_LT(...) DOCTEST_REQUIRE_LT(__VA_ARGS__) +#define WARN_GE(...) DOCTEST_WARN_GE(__VA_ARGS__) +#define CHECK_GE(...) DOCTEST_CHECK_GE(__VA_ARGS__) +#define REQUIRE_GE(...) DOCTEST_REQUIRE_GE(__VA_ARGS__) +#define WARN_LE(...) DOCTEST_WARN_LE(__VA_ARGS__) +#define CHECK_LE(...) DOCTEST_CHECK_LE(__VA_ARGS__) +#define REQUIRE_LE(...) DOCTEST_REQUIRE_LE(__VA_ARGS__) +#define WARN_UNARY(...) DOCTEST_WARN_UNARY(__VA_ARGS__) +#define CHECK_UNARY(...) DOCTEST_CHECK_UNARY(__VA_ARGS__) +#define REQUIRE_UNARY(...) DOCTEST_REQUIRE_UNARY(__VA_ARGS__) +#define WARN_UNARY_FALSE(...) DOCTEST_WARN_UNARY_FALSE(__VA_ARGS__) +#define CHECK_UNARY_FALSE(...) DOCTEST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define REQUIRE_UNARY_FALSE(...) DOCTEST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +// KEPT FOR BACKWARDS COMPATIBILITY +#define FAST_WARN_EQ(...) DOCTEST_FAST_WARN_EQ(__VA_ARGS__) +#define FAST_CHECK_EQ(...) DOCTEST_FAST_CHECK_EQ(__VA_ARGS__) +#define FAST_REQUIRE_EQ(...) DOCTEST_FAST_REQUIRE_EQ(__VA_ARGS__) +#define FAST_WARN_NE(...) DOCTEST_FAST_WARN_NE(__VA_ARGS__) +#define FAST_CHECK_NE(...) DOCTEST_FAST_CHECK_NE(__VA_ARGS__) +#define FAST_REQUIRE_NE(...) DOCTEST_FAST_REQUIRE_NE(__VA_ARGS__) +#define FAST_WARN_GT(...) DOCTEST_FAST_WARN_GT(__VA_ARGS__) +#define FAST_CHECK_GT(...) DOCTEST_FAST_CHECK_GT(__VA_ARGS__) +#define FAST_REQUIRE_GT(...) DOCTEST_FAST_REQUIRE_GT(__VA_ARGS__) +#define FAST_WARN_LT(...) DOCTEST_FAST_WARN_LT(__VA_ARGS__) +#define FAST_CHECK_LT(...) DOCTEST_FAST_CHECK_LT(__VA_ARGS__) +#define FAST_REQUIRE_LT(...) DOCTEST_FAST_REQUIRE_LT(__VA_ARGS__) +#define FAST_WARN_GE(...) DOCTEST_FAST_WARN_GE(__VA_ARGS__) +#define FAST_CHECK_GE(...) DOCTEST_FAST_CHECK_GE(__VA_ARGS__) +#define FAST_REQUIRE_GE(...) DOCTEST_FAST_REQUIRE_GE(__VA_ARGS__) +#define FAST_WARN_LE(...) DOCTEST_FAST_WARN_LE(__VA_ARGS__) +#define FAST_CHECK_LE(...) DOCTEST_FAST_CHECK_LE(__VA_ARGS__) +#define FAST_REQUIRE_LE(...) DOCTEST_FAST_REQUIRE_LE(__VA_ARGS__) + +#define FAST_WARN_UNARY(...) DOCTEST_FAST_WARN_UNARY(__VA_ARGS__) +#define FAST_CHECK_UNARY(...) DOCTEST_FAST_CHECK_UNARY(__VA_ARGS__) +#define FAST_REQUIRE_UNARY(...) DOCTEST_FAST_REQUIRE_UNARY(__VA_ARGS__) +#define FAST_WARN_UNARY_FALSE(...) DOCTEST_FAST_WARN_UNARY_FALSE(__VA_ARGS__) +#define FAST_CHECK_UNARY_FALSE(...) DOCTEST_FAST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define FAST_REQUIRE_UNARY_FALSE(...) DOCTEST_FAST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +#define TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_MACROS + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_LIBRARY_INCLUDED + +#if defined(DOCTEST_CONFIG_IMPLEMENT) && !defined(DOCTEST_LIBRARY_IMPLEMENTATION) + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-macros") +#define DOCTEST_LIBRARY_IMPLEMENTATION +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#ifndef DOCTEST_PARTS_PRIVATE_PRELUDE +#define DOCTEST_PARTS_PRIVATE_PRELUDE + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +// required includes - will go only in one translation unit! +#include +#include +#include +// borland (Embarcadero) compiler requires math.h and not cmath - +// https://github.com/doctest/doctest/pull/37 +#ifdef __BORLANDC__ +#include +#endif // __BORLANDC__ +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#include +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#include +#include +#include +#ifndef DOCTEST_CONFIG_NO_MULTITHREADING +#include +#include +#define DOCTEST_DECLARE_MUTEX(name) std::mutex name; +#define DOCTEST_DECLARE_STATIC_MUTEX(name) static DOCTEST_DECLARE_MUTEX(name) +#define DOCTEST_LOCK_MUTEX(name) const std::lock_guard DOCTEST_ANONYMOUS(DOCTEST_ANON_LOCK_)(name); +#else // DOCTEST_CONFIG_NO_MULTITHREADING +#define DOCTEST_DECLARE_MUTEX(name) +#define DOCTEST_DECLARE_STATIC_MUTEX(name) +#define DOCTEST_LOCK_MUTEX(name) +#endif // DOCTEST_CONFIG_NO_MULTITHREADING +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef DOCTEST_PLATFORM_MAC +#include +#include +#include +#endif // DOCTEST_PLATFORM_MAC + +#ifndef DOCTEST_PLATFORM_WINDOWS +#include +#include +#endif // DOCTEST_PLATFORM_WINDOWS + +// counts the number of elements in a C array +#define DOCTEST_COUNTOF(x) (sizeof(x) / sizeof(x[0])) + +#ifdef DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_disabled +#else // DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_not_disabled +#endif // DOCTEST_CONFIG_DISABLE + +#ifndef DOCTEST_THREAD_LOCAL +#if defined(DOCTEST_CONFIG_NO_MULTITHREADING) || DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_THREAD_LOCAL +#else // DOCTEST_MSVC +#define DOCTEST_THREAD_LOCAL thread_local +#endif // DOCTEST_MSVC +#endif // DOCTEST_THREAD_LOCAL + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES +#define DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES 32 +#endif + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE +#define DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE 64 +#endif + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_PRELUDE +#ifndef DOCTEST_PARTS_PRIVATE_CONTEXT_STATE +#define DOCTEST_PARTS_PRIVATE_CONTEXT_STATE + +#ifndef DOCTEST_PARTS_PRIVATE_TIMER +#define DOCTEST_PARTS_PRIVATE_TIMER + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +uint64_t getCurrentTicks(); + +struct Timer { + void start(); + unsigned int getElapsedMicroseconds() const; + double getElapsedSeconds() const; + +private: + uint64_t m_ticks = 0; +}; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_TIMER +#ifndef DOCTEST_PARTS_PRIVATE_ATOMIC +#define DOCTEST_PARTS_PRIVATE_ATOMIC + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_CONFIG_NO_MULTITHREADING +template +using Atomic = T; +#else // DOCTEST_CONFIG_NO_MULTITHREADING +template +using Atomic = std::atomic; +#endif // DOCTEST_CONFIG_NO_MULTITHREADING + +#if defined(DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS) || defined(DOCTEST_CONFIG_NO_MULTITHREADING) +template +using MultiLaneAtomic = Atomic; +#else // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS +// Provides a multilane implementation of an atomic variable that supports add, sub, load, +// store. Instead of using a single atomic variable, this splits up into multiple ones, +// each sitting on a separate cache line. The goal is to provide a speedup when most +// operations are modifying. It achieves this with two properties: +// +// * Multiple atomics are used, so chance of congestion from the same atomic is reduced. +// * Each atomic sits on a separate cache line, so false sharing is reduced. +// +// The disadvantage is that there is a small overhead due to the use of TLS, and load/store +// is slower because all atomics have to be accessed. +template +class MultiLaneAtomic { + struct CacheLineAlignedAtomic { + Atomic atomic{}; + char padding[DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE - sizeof(Atomic)]; + }; + CacheLineAlignedAtomic m_atomics[DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES]; + + static_assert( + sizeof(CacheLineAlignedAtomic) == DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE, + "guarantee one atomic takes exactly one cache line" + ); + +public: + T operator++() DOCTEST_NOEXCEPT { + return fetch_add(1) + 1; + } + + T operator++(int) DOCTEST_NOEXCEPT { // NOLINT(cert-dcl21-cpp) + return fetch_add(1); + } + + T fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_add(arg, order); + } + + T fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_sub(arg, order); + } + + operator T() const DOCTEST_NOEXCEPT { + return load(); + } + + T load(std::memory_order order = std::memory_order_seq_cst) const DOCTEST_NOEXCEPT { + auto result = T(); + for (const auto &c: m_atomics) { + result += c.atomic.load(order); + } + return result; + } + + // NOLINTNEXTLINE(cppcoreguidelines-c-copy-assignment-signature, misc-unconventional-assign-operator) + T operator=(T desired) DOCTEST_NOEXCEPT { + store(desired); + return desired; + } + + void store(T desired, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + // first value becomes desired", all others become 0. + for (auto &c: m_atomics) { + c.atomic.store(desired, order); + desired = {}; + } + } + +private: + // Each thread has a different atomic that it operates on. If more than NumLanes threads + // use this, some will use the same atomic. So performance will degrade a bit, but still + // everything will work. + // + // The logic here is a bit tricky. The call should be as fast as possible, so that there + // is minimal to no overhead in determining the correct atomic for the current thread. + // + // 1. A global static counter laneCounter counts continuously up. + // 2. Each successive thread will use modulo operation of that counter so it gets an atomic + // assigned in a round-robin fashion. + // 3. This tlsLaneIdx is stored in the thread local data, so it is directly available with + // little overhead. + Atomic &myAtomic() DOCTEST_NOEXCEPT { + static Atomic laneCounter; + DOCTEST_THREAD_LOCAL size_t tlsLaneIdx = laneCounter++ % DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES; + + return m_atomics[tlsLaneIdx].atomic; + } +}; +#endif // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_ATOMIC +#ifndef DOCTEST_PARTS_PRIVATE_TRAVERSAL +#define DOCTEST_PARTS_PRIVATE_TRAVERSAL + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +struct DecisionPoint { + // Number of branches available at this depth for the current traversal path. + size_t branch_count = 0; + // Encountered sibling subcases in source order for subcase decision points. + std::vector subcases; +}; + +class TraversalState { +public: + size_t activeSubcaseDepth() const { + return m_activeSubcaseDepth; + } + + void resetForTestCase(); + void resetForRun(); + bool advance(); + bool tryEnterSubcase(const SubcaseSignature &signature); + void leaveSubcase(); + size_t unwindActiveSubcases(); + size_t acquireGeneratorIndex(size_t count); + +private: + // decisionPath is the selected traversal prefix; discoveredDecisionPath is rebuilt + // on each rerun to describe the branches encountered at each depth. + std::vector m_discoveredDecisionPath; + std::vector m_decisionPath; + size_t m_decisionDepth = 0; + std::vector m_enteredSubcaseDepths; + size_t m_activeSubcaseDepth = 0; + + DecisionPoint &ensureDecisionPointAtCurrentDepth(); +}; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_TRAVERSAL + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// this holds both parameters from the command line and runtime data for tests +struct ContextState : ContextOptions, TestRunStats, CurrentTestCaseStats { + MultiLaneAtomic numAssertsCurrentTest_atomic; + MultiLaneAtomic numAssertsFailedCurrentTest_atomic; + + std::vector> filters = decltype(filters)(9); // 9 different filters + + std::vector reporters_currently_used; + + assert_handler ah = nullptr; + + Timer timer; + + std::vector stringifiedContexts; // logging from INFO() due to an exception + + // Backtrack traversal state shared by SUBCASE and GENERATE. + TraversalState traversal; + Atomic shouldLogCurrentException; + + void resetRunData(); + + void finalizeTestCaseData(); +}; + +extern ContextState *g_cs; + +// used to avoid locks for the debug output +// TODO: figure out if this is indeed necessary/correct - seems like either there still +// could be a race or that there wouldn't be a race even if using the context directly +extern DOCTEST_THREAD_LOCAL bool g_no_colors; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_CONTEXT_STATE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +using detail::g_cs; + +AssertData::AssertData( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const StringContains &exception_string +) + : m_test_case(g_cs->currentTest), + m_at(at), + m_file(file), + m_line(line), + m_expr(expr), + m_failed(true), + m_threw(false), + m_threw_as(false), + m_exception_type(exception_type), + m_exception_string(exception_string) { +#if DOCTEST_MSVC + if (m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC + ++m_expr; +#endif // MSVC +} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +ExpressionDecomposer::ExpressionDecomposer(assertType::Enum at) + : m_at(at) {} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_REPORTER +#define DOCTEST_PARTS_PRIVATE_REPORTER + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { +// the int (priority) is part of the key for automatic sorting - sadly one can register a +// reporter with a duplicate name and a different priority but hopefully that won't happen often :| +using reporterMap = std::map, detail::reporterCreatorFunc>; + +reporterMap &getReporters() noexcept; +reporterMap &getListeners() noexcept; +} // namespace detail + +#define DOCTEST_ITERATE_THROUGH_REPORTERS(function, ...) \ + for (auto &curr_rep: g_cs->reporters_currently_used) \ + curr_rep->function(__VA_ARGS__) + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTER +#ifndef DOCTEST_PARTS_PRIVATE_ASSERT_HANDLER +#define DOCTEST_PARTS_PRIVATE_ASSERT_HANDLER + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +void addAssert(assertType::Enum at); + +void addFailedAssert(assertType::Enum at); + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_ASSERT_HANDLER + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +void addAssert(assertType::Enum at) { + if ((at & assertType::is_warn) == 0) + g_cs->numAssertsCurrentTest_atomic++; +} + +void addFailedAssert(assertType::Enum at) { + if ((at & assertType::is_warn) == 0) + g_cs->numAssertsFailedCurrentTest_atomic++; +} + +void failed_out_of_a_testing_context(const AssertData &ad) { + if (g_cs->ah) + g_cs->ah(ad); + else + std::abort(); +} + +bool decomp_assert(assertType::Enum at, const char *file, int line, const char *expr, const Result &result) { + const bool failed = !result.m_passed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(result.m_decomp); + DOCTEST_ASSERT_IN_TESTS(result.m_decomp); + return !failed; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +MessageBuilder::MessageBuilder(const char *file, int line, assertType::Enum severity) { + m_stream = tlssPush(); + m_file = file; + m_line = line; + m_severity = severity; +} + +MessageBuilder::~MessageBuilder() noexcept(false) { + if (!logged) + tlssPop(); +} + +bool MessageBuilder::log() { + if (!logged) { + m_string = tlssPop(); + logged = true; + } + + DOCTEST_ITERATE_THROUGH_REPORTERS(log_message, *this); + + const bool isWarn = m_severity & assertType::is_warn; + + // warn is just a message in this context so we don't treat it as an assert + if (!isWarn) { + addAssert(m_severity); + addFailedAssert(m_severity); + } + + return isDebuggerActive() && !getContextOptions()->no_breaks && !isWarn && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger +} + +void MessageBuilder::react() { + if (m_severity & assertType::is_require) + throwException(); +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_EXCEPTION_TRANSLATOR +#define DOCTEST_PARTS_PRIVATE_EXCEPTION_TRANSLATOR + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +std::vector &getExceptionTranslators() noexcept; +String translateActiveException() noexcept; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_EXCEPTION_TRANSLATOR + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +Result::Result(bool passed, const String &decomposition) + : m_passed(passed), m_decomp(decomposition) {} + +ResultBuilder::ResultBuilder( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const String &exception_string +) + : AssertData(at, file, line, expr, exception_type, exception_string) { +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +ResultBuilder::ResultBuilder( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const Contains &exception_string +) + : AssertData(at, file, line, expr, exception_type, exception_string) {} + +void ResultBuilder::setResult(const Result &res) { + m_decomp = res.m_decomp; + m_failed = !res.m_passed; +} + +void ResultBuilder::translateException() { + m_threw = true; + m_exception = translateActiveException(); +} + +bool ResultBuilder::log() { + if (m_at & assertType::is_throws) { + m_failed = !m_threw; + } else if ((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { + m_failed = !m_threw_as || !m_exception_string.check(m_exception); + } else if (m_at & assertType::is_throws_as) { + m_failed = !m_threw_as; + } else if (m_at & assertType::is_throws_with) { + m_failed = !m_exception_string.check(m_exception); + } else if (m_at & assertType::is_nothrow) { + m_failed = m_threw; + } + + if (m_exception.size()) + m_exception = "\"" + m_exception + "\""; + + if (is_running_in_test) { + addAssert(m_at); + DOCTEST_ITERATE_THROUGH_REPORTERS(log_assert, *this); + + if (m_failed) + addFailedAssert(m_at); + } else if (m_failed) { + failed_out_of_a_testing_context(*this); + } + + return m_failed && isDebuggerActive() && !getContextOptions()->no_breaks && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger +} + +void ResultBuilder::react() const { + if (m_failed && checkIfShouldThrow(m_at)) + throwException(); +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_EXCEPTIONS +#define DOCTEST_PARTS_PRIVATE_EXCEPTIONS + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +/** + * Compatibility wrapper around: + * - std::uncaught_exception + * - std::uncaught_exceptions + * ...depending on the availability of each function + */ +bool has_uncaught_exceptions(); + +template +DOCTEST_NORETURN void throw_exception(const Ex &e) { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + throw e; +#else // DOCTEST_CONFIG_NO_EXCEPTIONS +#ifdef DOCTEST_CONFIG_HANDLE_EXCEPTION + DOCTEST_CONFIG_HANDLE_EXCEPTION(e); +#else // DOCTEST_CONFIG_HANDLE_EXCEPTION +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + std::cerr << "doctest will terminate because it needed to throw an exception.\n" + << "The message was: " << e.what() << '\n'; +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#endif // DOCTEST_CONFIG_HANDLE_EXCEPTION + std::terminate(); +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +} + +#ifndef DOCTEST_INTERNAL_ERROR +#define DOCTEST_INTERNAL_ERROR(msg) \ + detail::throw_exception(std::logic_error(__FILE__ ":" DOCTEST_TOSTR(__LINE__) ": Internal doctest error: " msg)) +#endif // DOCTEST_INTERNAL_ERROR +} // namespace detail + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_EXCEPTIONS + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +const char *assertString(assertType::Enum at) { + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4061) // enum 'x' in switch of enum 'y' is not explicitly handled +#define DOCTEST_GENERATE_ASSERT_TYPE_CASE(assert_type) \ + case assertType::DT_##assert_type: return #assert_type +#define DOCTEST_GENERATE_ASSERT_TYPE_CASES(assert_type) \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN_##assert_type); \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK_##assert_type); \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE_##assert_type) + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") + DOCTEST_GCC_SUPPRESS_WARNING_PUSH + DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") + switch (at) { + DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN); + DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK); + DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(FALSE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_AS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH_AS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(NOTHROW); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(EQ); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(NE); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(GT); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(LT); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(GE); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(LE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY_FALSE); + + default: DOCTEST_INTERNAL_ERROR("Tried stringifying invalid assert type!"); + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP +} + +const char *failureString(assertType::Enum at) { + if (at & assertType::is_warn) + return "WARNING"; + if (at & assertType::is_check) + return "ERROR"; + if (at & assertType::is_require) + return "FATAL ERROR"; + return ""; +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_EXT_WINDOWS +#define DOCTEST_PARTS_PRIVATE_EXT_WINDOWS + + +// TODO: This wrapper should really be applied at the call site, but there's a bug in our Codecov +// setup causing conditional guards around includes to conflict with `--remap`. +#ifdef DOCTEST_PLATFORM_WINDOWS + +// Internal windows headers require explicit supressions to handle `-weverything`. +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnonportable-system-include-path") +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4265) // virtual functions, but destructor is not virtual +DOCTEST_MSVC_SUPPRESS_WARNING(4350) // 'member1' called instead of 'member2' +DOCTEST_MSVC_SUPPRESS_WARNING(4548) // before comma no effect; expected side - effect +DOCTEST_MSVC_SUPPRESS_WARNING(4623) // default constructor was implicitly deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4625) // copy constructor was implicitly deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4626) // assignment operator was implicitly deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4668) // not defined as a preprocessor macro +DOCTEST_MSVC_SUPPRESS_WARNING(4774) // format string not a string literal +DOCTEST_MSVC_SUPPRESS_WARNING(4820) // padding +DOCTEST_MSVC_SUPPRESS_WARNING(4865) // the underlying type will change if '/Zc:enumTypes' is passed +DOCTEST_MSVC_SUPPRESS_WARNING(4986) // exception specification does not match previous +DOCTEST_MSVC_SUPPRESS_WARNING(5026) // move constructor was implicitly deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5027) // move assignment operator implicitly deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5039) // pointer to pot. throwing function passed to extern C +DOCTEST_MSVC_SUPPRESS_WARNING(5045) // Spectre mitigation for memory load +DOCTEST_MSVC_SUPPRESS_WARNING(5105) // macro producing 'defined' has undefined behavior +DOCTEST_MSVC_SUPPRESS_WARNING(5262) // implicit fall-through + +// defines for a leaner windows.h +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#define DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#define DOCTEST_UNDEF_NOMINMAX +#endif + +// not sure what AfxWin.h is for - here I do what Catch does +#ifdef __AFXDLL +#include // IWYU pragma: export +#else +#include // IWYU pragma: export +#endif + +#ifdef DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#undef DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#endif +#ifdef DOCTEST_UNDEF_NOMINMAX +#undef NOMINMAX +#undef DOCTEST_UNDEF_NOMINMAX +#endif + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_PLATFORM_WINDOWS + +#endif // DOCTEST_PARTS_PRIVATE_EXT_WINDOWS + +#ifdef DOCTEST_PLATFORM_WINDOWS +#include +#endif + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#if !defined(DOCTEST_CONFIG_COLORS_NONE) +#if !defined(DOCTEST_CONFIG_COLORS_WINDOWS) && !defined(DOCTEST_CONFIG_COLORS_ANSI) +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_CONFIG_COLORS_WINDOWS +#else // linux +#define DOCTEST_CONFIG_COLORS_ANSI +#endif // platform +#endif // DOCTEST_CONFIG_COLORS_WINDOWS && DOCTEST_CONFIG_COLORS_ANSI +#endif // DOCTEST_CONFIG_COLORS_NONE + +// this is a fix for https://github.com/doctest/doctest/issues/348 +// https://mail.gnome.org/archives/xml/2012-January/msg00000.html +#if !defined(HAVE_UNISTD_H) && !defined(STDOUT_FILENO) +#define STDOUT_FILENO fileno(stdout) +#endif // HAVE_UNISTD_H + +namespace doctest { + +namespace detail { +void color_to_stream(std::ostream &, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;) +} // namespace detail + +namespace Color { +std::ostream &operator<<(std::ostream &s, Color::Enum code) { + detail::color_to_stream(s, code); + return s; +} +} // namespace Color + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") +void color_to_stream(std::ostream &s, Color::Enum code) { + static_cast(s); // for DOCTEST_CONFIG_COLORS_NONE or DOCTEST_CONFIG_COLORS_WINDOWS + static_cast(code); // for DOCTEST_CONFIG_COLORS_NONE +#ifdef DOCTEST_CONFIG_COLORS_ANSI + if (g_no_colors || (isatty(STDOUT_FILENO) == false && getContextOptions()->force_colors == false)) + return; + + auto col = ""; // NOLINT(clang-analyzer-deadcode.DeadStores) + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") + switch (code) { + case Color::Red: col = "[0;31m"; break; + case Color::Green: col = "[0;32m"; break; + case Color::Blue: col = "[0;34m"; break; + case Color::Cyan: col = "[0;36m"; break; + case Color::Yellow: col = "[0;33m"; break; + case Color::Grey: col = "[1;30m"; break; + case Color::LightGrey: col = "[0;37m"; break; + case Color::BrightRed: col = "[1;31m"; break; + case Color::BrightGreen: col = "[1;32m"; break; + case Color::BrightWhite: col = "[1;37m"; break; + case Color::Bright: // invalid + case Color::None: + case Color::White: + default: col = "[0m"; + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + s << "\033" << col; +#endif // DOCTEST_CONFIG_COLORS_ANSI + +#ifdef DOCTEST_CONFIG_COLORS_WINDOWS + if (g_no_colors || (_isatty(_fileno(stdout)) == false && getContextOptions()->force_colors == false)) + return; + + static struct ConsoleHelper { + HANDLE stdoutHandle; + WORD origFgAttrs; + WORD origBgAttrs; + + ConsoleHelper() { + stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO csbiInfo; + GetConsoleScreenBufferInfo(stdoutHandle, &csbiInfo); + origFgAttrs = + csbiInfo.wAttributes & ~(BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_INTENSITY); + origBgAttrs = + csbiInfo.wAttributes & ~(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY); + } + } ch; + +#define DOCTEST_SET_ATTR(x) SetConsoleTextAttribute(ch.stdoutHandle, x | ch.origBgAttrs) + + // clang-format off + switch (code) { + case Color::White: DOCTEST_SET_ATTR(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::Red: DOCTEST_SET_ATTR(FOREGROUND_RED); break; + case Color::Green: DOCTEST_SET_ATTR(FOREGROUND_GREEN); break; + case Color::Blue: DOCTEST_SET_ATTR(FOREGROUND_BLUE); break; + case Color::Cyan: DOCTEST_SET_ATTR(FOREGROUND_BLUE | FOREGROUND_GREEN); break; + case Color::Yellow: DOCTEST_SET_ATTR(FOREGROUND_RED | FOREGROUND_GREEN); break; + case Color::Grey: DOCTEST_SET_ATTR(0); break; + case Color::LightGrey: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY); break; + case Color::BrightRed: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_RED); break; + case Color::BrightGreen: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN); break; + case Color::BrightWhite: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::None: + case Color::Bright: // invalid + default: DOCTEST_SET_ATTR(ch.origFgAttrs); + } + // clang-format on +#endif // DOCTEST_CONFIG_COLORS_WINDOWS +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLED +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +const ContextOptions *getContextOptions() { + return DOCTEST_BRANCH_ON_DISABLED(nullptr, detail::g_cs); +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_COMMON +#define DOCTEST_PARTS_PRIVATE_REPORTERS_COMMON + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_OPTIONS_PREFIX +#define DOCTEST_CONFIG_OPTIONS_PREFIX "dt-" +#endif + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +void fulltext_log_assert_to_stream(std::ostream &s, const AssertData &rb); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_COMMON +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_DEBUG_OUTPUT_WINDOW +#define DOCTEST_PARTS_PRIVATE_REPORTERS_DEBUG_OUTPUT_WINDOW + +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_CONSOLE +#define DOCTEST_PARTS_PRIVATE_REPORTERS_CONSOLE + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifdef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS +#define DOCTEST_OPTIONS_PREFIX_DISPLAY DOCTEST_CONFIG_OPTIONS_PREFIX +#else +#define DOCTEST_OPTIONS_PREFIX_DISPLAY "" +#endif + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +struct Whitespace { + int nrSpaces; + explicit Whitespace(int nr); +}; + +std::ostream &operator<<(std::ostream &out, const Whitespace &ws); + +struct ConsoleReporter : public IReporter { + std::ostream &s; + bool hasLoggedCurrentTestStart; + std::vector subcasesStack; + size_t currentSubcaseLevel; + DOCTEST_DECLARE_MUTEX(mutex) + + // caching pointers/references to objects of these types - safe to do + const ContextOptions &opt; + const TestCaseData *tc; + + ConsoleReporter(const ContextOptions &co); + + ConsoleReporter(const ContextOptions &co, std::ostream &ostr); + + // ========================================================================================= + // WHAT FOLLOWS ARE HELPERS USED BY THE OVERRIDES OF THE VIRTUAL METHODS OF THE INTERFACE + // ========================================================================================= + + void separator_to_stream(); + + static const char *getSuccessOrFailString(bool success, assertType::Enum at, const char *success_str); + + static Color::Enum getSuccessOrFailColor(bool success, assertType::Enum at); + + void successOrFailColoredStringToStream(bool success, assertType::Enum at, const char *success_str = "SUCCESS"); + + void log_contexts(); + + // this was requested to be made virtual so users could override it + virtual void file_line_to_stream(const char *file, int line, const char *tail = ""); + + void logTestStart(); + + void printVersion(); + + void printIntro(); + + void printHelp(); + + void printRegisteredReporters(); + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData &in) override; + + void test_run_start() override; + + void test_run_end(const TestRunStats &p) override; + + void test_case_start(const TestCaseData &in) override; + + void test_case_reenter(const TestCaseData &) override; + + void test_case_end(const CurrentTestCaseStats &st) override; + + void test_case_exception(const TestCaseException &e) override; + + void subcase_start(const SubcaseSignature &subc) override; + + void subcase_end() override; + + void log_assert(const AssertData &rb) override; + + void log_message(const MessageData &mb) override; + + void test_case_skipped(const TestCaseData &) override; +}; + +DOCTEST_REGISTER_REPORTER("console", 0, ConsoleReporter); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_CONSOLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_PLATFORM_WINDOWS +struct DebugOutputWindowReporter : public ConsoleReporter { + DOCTEST_THREAD_LOCAL static std::ostringstream oss; + + DebugOutputWindowReporter(const ContextOptions &co); + + void test_run_start() override; + void test_run_end(const TestRunStats &in) override; + void test_case_start(const TestCaseData &in) override; + void test_case_reenter(const TestCaseData &in) override; + void test_case_end(const CurrentTestCaseStats &in) override; + void test_case_exception(const TestCaseException &in) override; + void subcase_start(const SubcaseSignature &in) override; + void subcase_end(DOCTEST_EMPTY DOCTEST_EMPTY) override; + void log_assert(const AssertData &in) override; + void log_message(const MessageData &in) override; + void test_case_skipped(const TestCaseData &in) override; +}; +#endif // DOCTEST_PLATFORM_WINDOWS + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_DEBUG_OUTPUT_WINDOW +#ifndef DOCTEST_PARTS_PRIVATE_TEST_CASE +#define DOCTEST_PARTS_PRIVATE_TEST_CASE + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// all the registered tests +std::set &getRegisteredTests(); + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_TEST_CASE +#ifndef DOCTEST_PARTS_PRIVATE_FILTERS +#define DOCTEST_PARTS_PRIVATE_FILTERS + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// matching of a string against a wildcard mask (case sensitivity configurable) taken from +// https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing +int wildcmp(const char *str, const char *wild, bool caseSensitive); + +// checks if the name matches any of the filters (and can be configured what to do when empty) +bool matchesAny(const char *name, const std::vector &filters, bool matchEmpty, bool caseSensitive); + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_FILTERS +#ifndef DOCTEST_PARTS_PRIVATE_SIGNALS +#define DOCTEST_PARTS_PRIVATE_SIGNALS + + +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) +#ifndef DOCTEST_PLATFORM_WINDOWS +#include +#endif +#endif + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +struct FatalConditionHandler { + static void reset(); + static void allocateAltStackMem(); + static void freeAltStackMem(); +}; +#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +void reportFatal(const std::string &message); + +#ifdef DOCTEST_PLATFORM_WINDOWS + +#ifndef DOCTEST_CDECL +#define DOCTEST_CDECL __cdecl +#endif + +struct SignalDefs { + DWORD id; + const char *name; +}; + +struct FatalConditionHandler { + static LONG CALLBACK handleException(PEXCEPTION_POINTERS ExceptionInfo); + static void allocateAltStackMem(); + static void freeAltStackMem(); + + FatalConditionHandler(); + + static void reset(); + + ~FatalConditionHandler(); + +private: + static UINT prev_error_mode_1; + static int prev_error_mode_2; + static unsigned int prev_abort_behavior; + static int prev_report_mode; + static _HFILE prev_report_file; + static void(DOCTEST_CDECL *prev_sigabrt_handler)(int); + static std::terminate_handler original_terminate_handler; + static bool isSet; + static ULONG guaranteeSize; + static LPTOP_LEVEL_EXCEPTION_FILTER previousTop; +}; + +#else // DOCTEST_PLATFORM_WINDOWS + +struct SignalDefs { + int id; + const char *name; +}; + +struct FatalConditionHandler { + static bool isSet; + static struct sigaction oldSigActions[6]; + static stack_t oldSigStack; + static size_t altStackSize; + static char *altStackMem; + + static void handleSignal(int sig); + + static void allocateAltStackMem(); + + static void freeAltStackMem(); + + FatalConditionHandler(); + + ~FatalConditionHandler(); + static void reset(); +}; + +#endif // DOCTEST_PLATFORM_WINDOWS +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_SIGNALS + +// Fix for #1035 +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_JUNIT +#define DOCTEST_PARTS_PRIVATE_REPORTERS_JUNIT + +#ifndef DOCTEST_PARTS_PRIVATE_XML +#define DOCTEST_PARTS_PRIVATE_XML + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= +/* clang-format off */ /* NOLINTBEGIN */ + + class XmlEncode { + public: + enum ForWhat { ForTextNodes, ForAttributes }; + + XmlEncode( std::string const& str, ForWhat forWhat = ForTextNodes ); + + void encodeTo( std::ostream& os ) const; + + friend std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ); + + private: + std::string m_str; + ForWhat m_forWhat; + }; + + class XmlWriter { + public: + + class ScopedElement { + public: + ScopedElement( XmlWriter* writer ); + + ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT; + ScopedElement& operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT; + + ~ScopedElement(); + + ScopedElement& writeText( std::string const& text, bool indent = true ); + + template + ScopedElement& writeAttribute( std::string const& name, T const& attribute ) { + m_writer->writeAttribute( name, attribute ); + return *this; + } + + private: + mutable XmlWriter* m_writer = nullptr; + }; + +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + XmlWriter( std::ostream& os = std::cout ); +#else // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + XmlWriter( std::ostream& os ); +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + ~XmlWriter(); + + XmlWriter( XmlWriter const& ) = delete; + XmlWriter& operator=( XmlWriter const& ) = delete; + + XmlWriter& startElement( std::string const& name ); + + ScopedElement scopedElement( std::string const& name ); + + XmlWriter& endElement(); + + XmlWriter& writeAttribute( std::string const& name, std::string const& attribute ); + + XmlWriter& writeAttribute( std::string const& name, const char* attribute ); + + XmlWriter& writeAttribute( std::string const& name, bool attribute ); + + template + XmlWriter& writeAttribute( std::string const& name, T const& attribute ) { + std::stringstream rss; + rss << attribute; + return writeAttribute( name, rss.str() ); + } + + XmlWriter& writeText( std::string const& text, bool indent = true ); + + //XmlWriter& writeComment( std::string const& text ); + + //void writeStylesheetRef( std::string const& url ); + + //XmlWriter& writeBlankLine(); + + void ensureTagClosed(); + + void writeDeclaration(); + + private: + + void newlineIfNecessary(); + + bool m_tagIsOpen = false; + bool m_needsNewline = false; + std::vector m_tags; + std::string m_indent; + std::ostream& m_os; + }; + +/* clang-format on */ /* NOLINTEND */ +// ================================================================================================= +// End of copy-pasted code from Catch +// ================================================================================================= + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_XML + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +// TODO: +// - log_message() +// - respond to queries +// - honor remaining options +// - more attributes in tags +struct JUnitReporter : public IReporter { + detail::XmlWriter xml; + DOCTEST_DECLARE_MUTEX(mutex) + detail::Timer timer; + std::vector deepestSubcaseStackNames; + + struct JUnitTestCaseData { + static std::string getCurrentTimestamp(); + + struct JUnitTestMessage { + JUnitTestMessage(const std::string &_message, const std::string &_type, const std::string &_details); + + JUnitTestMessage(const std::string &_message, const std::string &_details); + + std::string message, type, details; + }; + + struct JUnitTestCase { + JUnitTestCase(const std::string &_classname, const std::string &_name); + + std::string classname, name; + double time; + std::vector failures, errors; + }; + + void add(const std::string &classname, const std::string &name); + + void appendSubcaseNamesToLastTestcase(std::vector nameStack); + + void addTime(double time); + + void addFailure(const std::string &message, const std::string &type, const std::string &details); + + void addError(const std::string &message, const std::string &details); + + std::vector testcases; + double totalSeconds = 0; + int totalErrors = 0, totalFailures = 0; + }; + + JUnitTestCaseData testCaseData; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions &opt; + const TestCaseData *tc = nullptr; + + JUnitReporter(const ContextOptions &co); + + unsigned line(unsigned l) const; + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData &) override; + + void test_run_start() override; + + void test_run_end(const TestRunStats &p) override; + + void test_case_start(const TestCaseData &in) override; + + void test_case_reenter(const TestCaseData &in) override; + + void test_case_end(const CurrentTestCaseStats &) override; + + void test_case_exception(const TestCaseException &e) override; + + void subcase_start(const SubcaseSignature &in) override; + + void subcase_end() override; + + void log_assert(const AssertData &rb) override; + + void log_message(const MessageData &mb) override; + + void test_case_skipped(const TestCaseData &) override; + + static void log_contexts(std::ostringstream &s); +}; + +DOCTEST_REGISTER_REPORTER("junit", 0, JUnitReporter); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_JUNIT +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_XML +#define DOCTEST_PARTS_PRIVATE_REPORTERS_XML + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +struct XmlReporter : public IReporter { + detail::XmlWriter xml; + DOCTEST_DECLARE_MUTEX(mutex) + + // caching pointers/references to objects of these types - safe to do + const ContextOptions &opt; + const TestCaseData *tc = nullptr; + + XmlReporter(const ContextOptions &co); + + void log_contexts(); + + unsigned line(unsigned l) const; + + void test_case_start_impl(const TestCaseData &in); + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData &in) override; + + void test_run_start() override; + + void test_run_end(const TestRunStats &p) override; + + void test_case_start(const TestCaseData &in) override; + + void test_case_reenter(const TestCaseData &) override; + + void test_case_end(const CurrentTestCaseStats &st) override; + + void test_case_exception(const TestCaseException &e) override; + + void subcase_start(const SubcaseSignature &in) override; + + void subcase_end() override; + + void log_assert(const AssertData &rb) override; + + void log_message(const MessageData &mb) override; + + void test_case_skipped(const TestCaseData &in) override; +}; + +DOCTEST_REGISTER_REPORTER("xml", 0, XmlReporter); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_XML + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +bool is_running_in_test = false; + +#ifdef DOCTEST_CONFIG_DISABLE + +// NOLINTBEGIN(readability-convert-member-functions-to-static) +Context::Context(int, const char *const *) {} +Context::~Context() = default; +void Context::applyCommandLine(int, const char *const *) {} +void Context::addFilter(const char *, const char *) {} +void Context::clearFilters() {} +void Context::setOption(const char *, bool) {} +void Context::setOption(const char *, int) {} +void Context::setOption(const char *, const char *) {} +bool Context::shouldExit() { + return false; +} +void Context::setAsDefaultForAssertsOutOfTestCases() {} +void Context::setAssertHandler(detail::assert_handler) {} +void Context::setCout(std::ostream *) {} +int Context::run() { + return 0; +} +// NOLINTEND(readability-convert-member-functions-to-static) + +#else + +namespace detail { +// for sorting tests by file/line +bool fileOrderComparator(const TestCase *lhs, const TestCase *rhs) { + // this is needed because MSVC gives different case for drive letters + // for __FILE__ when evaluated in a header and a source file + const int res = lhs->m_file.compare(rhs->m_file, static_cast(DOCTEST_MSVC)); + if (res != 0) + return res < 0; + if (lhs->m_line != rhs->m_line) + return lhs->m_line < rhs->m_line; + return lhs->m_template_id < rhs->m_template_id; +} + +// for sorting tests by suite/file/line +bool suiteOrderComparator(const TestCase *lhs, const TestCase *rhs) { + const int res = std::strcmp(lhs->m_test_suite, rhs->m_test_suite); + if (res != 0) + return res < 0; + return fileOrderComparator(lhs, rhs); +} + +// for sorting tests by name/suite/file/line +bool nameOrderComparator(const TestCase *lhs, const TestCase *rhs) { + const int res = std::strcmp(lhs->m_name, rhs->m_name); + if (res != 0) + return res < 0; + return suiteOrderComparator(lhs, rhs); +} + +// the implementation of parseOption() +bool parseOptionImpl(int argc, const char *const *argv, const char *pattern, String *value) { + // going from the end to the beginning and stopping on the first occurrence from the end + for (int i = argc; i > 0; --i) { + auto index = i - 1; + auto temp = std::strstr(argv[index], pattern); + if (temp && (value || strlen(temp) == strlen(pattern))) { + // eliminate matches in which the chars before the option are not '-' + bool noBadCharsFound = true; + auto curr = argv[index]; + while (curr != temp) { + if (*curr++ != '-') { + noBadCharsFound = false; + break; + } + } + if (noBadCharsFound && argv[index][0] == '-') { + if (value) { + // parsing the value of an option + temp += strlen(pattern); + const unsigned len = strlen(temp); + if (len) { + *value = temp; + return true; + } + } else { + // just a flag - no value + return true; + } + } + } + } + return false; +} + +// parses an option and returns the string after the '=' character +bool parseOption( + int argc, const char *const *argv, const char *pattern, String *value = nullptr, const String &defaultVal = String() +) { + if (value) + *value = defaultVal; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + // offset (normally 3 for "dt-") to skip prefix + if (parseOptionImpl(argc, argv, pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX), value)) + return true; +#endif // DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + return parseOptionImpl(argc, argv, pattern, value); +} + +// locates a flag on the command line +bool parseFlag(int argc, const char *const *argv, const char *pattern) { + return parseOption(argc, argv, pattern); +} + +// parses a comma separated list of words after a pattern in one of the arguments in argv +bool parseCommaSepArgs(int argc, const char *const *argv, const char *pattern, std::vector &res) { + String filtersString; + if (parseOption(argc, argv, pattern, &filtersString)) { + // tokenize with "," as a separator, unless escaped with backslash + std::ostringstream s; + auto flush = [&s, &res]() { + auto string = s.str(); + if (!string.empty()) { + res.emplace_back(string.c_str()); + } + s.str(""); + }; + + bool seenBackslash = false; + const char *current = filtersString.c_str(); + const char *end = current + strlen(current); + while (current != end) { + const char character = *current++; + if (seenBackslash) { + seenBackslash = false; + if (character == ',' || character == '\\') { + s.put(character); + continue; + } + s.put('\\'); + } + if (character == '\\') { + seenBackslash = true; + } else if (character == ',') { + flush(); + } else { + s.put(character); + } + } + + if (seenBackslash) { + s.put('\\'); + } + flush(); + return true; + } + return false; +} + +enum optionType { option_bool, option_int }; + +// parses an int/bool option from the command line +bool parseIntOption(int argc, const char *const *argv, const char *pattern, optionType type, int &res) { + String parsedValue; + if (!parseOption(argc, argv, pattern, &parsedValue)) + return false; + + if (type) { + // integer + // TODO: change this to use std::stoi or something else! currently it uses undefined + // behavior - assumes '0' on failed parse... + // NOLINTNEXTLINE(bugprone-unchecked-string-to-number-conversion, cert-err34-c) + const int theInt = std::atoi(parsedValue.c_str()); + if (theInt != 0) { + res = theInt; + return true; + } + } else { + // boolean + const char positive[][5] = {"1", "true", "on", "yes"}; // 5 - strlen("true") + 1 + const char negative[][6] = {"0", "false", "off", "no"}; // 6 - strlen("false") + 1 + + // if the value matches any of the positive/negative possibilities + for (unsigned i = 0; i < 4; i++) { + if (parsedValue.compare(positive[i], true) == 0) { + res = 1; + return true; + } + if (parsedValue.compare(negative[i], true) == 0) { + res = 0; + return true; + } + } + } + return false; +} + +} // namespace detail + +Context::Context(int argc, const char *const *argv) + : p(new detail::ContextState) { + parseArgs(argc, argv, true); + if (argc) + p->binary_name = argv[0]; +} + +Context::~Context() { + if (detail::g_cs == p) + detail::g_cs = nullptr; + delete p; +} + +void Context::applyCommandLine(int argc, const char *const *argv) { + parseArgs(argc, argv); + if (argc) + p->binary_name = argv[0]; +} + +// parses args +void Context::parseArgs(int argc, const char *const *argv, bool withDefaults) { + using namespace detail; + + // clang-format off + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sf=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file-exclude=",p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sfe=", p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ts=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite-exclude=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tse=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tc=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case-exclude=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tce=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sc=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase-exclude=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sce=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "reporters=", p->filters[8]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "r=", p->filters[8]); + // clang-format on + + int intRes = 0; + String strRes; + +#define DOCTEST_PARSE_AS_BOOL_OR_FLAG(name, sname, var, default) \ + if (parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_bool, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_bool, intRes)) \ + p->var = static_cast(intRes); \ + else if ( \ + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name) || \ + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname) \ + ) \ + p->var = true; \ + else if (withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_INT_OPTION(name, sname, var, default) \ + if (parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_int, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_int, intRes)) \ + p->var = intRes; \ + else if (withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_STR_OPTION(name, sname, var, default) \ + if (parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", &strRes, default) || \ + parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", &strRes, default) || withDefaults) \ + p->var = strRes + + DOCTEST_PARSE_STR_OPTION("out", "o", out, ""); + DOCTEST_PARSE_STR_OPTION("order-by", "ob", order_by, "file"); + DOCTEST_PARSE_INT_OPTION("rand-seed", "rs", rand_seed, 0); + + DOCTEST_PARSE_INT_OPTION("first", "f", first, 0); + DOCTEST_PARSE_INT_OPTION("last", "l", last, UINT_MAX); + + DOCTEST_PARSE_INT_OPTION("abort-after", "aa", abort_after, 0); + DOCTEST_PARSE_INT_OPTION("subcase-filter-levels", "scfl", subcase_filter_levels, INT_MAX); + + DOCTEST_PARSE_AS_BOOL_OR_FLAG("success", "s", success, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("case-sensitive", "cs", case_sensitive, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("exit", "e", exit, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("duration", "d", duration, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("minimal", "m", minimal, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("quiet", "q", quiet, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-throw", "nt", no_throw, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-exitcode", "ne", no_exitcode, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-run", "nr", no_run, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-intro", "ni", no_intro, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-version", "nv", no_version, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-colors", "nc", no_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("force-colors", "fc", force_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-breaks", "nb", no_breaks, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skip", "ns", no_skip, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("gnu-file-line", "gfl", gnu_file_line, !bool(DOCTEST_MSVC)); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-path-filenames", "npf", no_path_in_filenames, false); + DOCTEST_PARSE_STR_OPTION("strip-file-prefixes", "sfp", strip_file_prefixes, ""); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-line-numbers", "nln", no_line_numbers, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-debug-output", "ndo", no_debug_output, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skipped-summary", "nss", no_skipped_summary, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-time-in-output", "ntio", no_time_in_output, false); + + if (withDefaults) { + p->help = false; + p->version = false; + p->count = false; + p->list_test_cases = false; + p->list_test_suites = false; + p->list_reporters = false; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "help") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "h") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "?")) { + p->help = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "version") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "v")) { + p->version = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "count") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "c")) { + p->count = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-cases") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ltc")) { + p->list_test_cases = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-suites") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lts")) { + p->list_test_suites = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-reporters") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lr")) { + p->list_reporters = true; + p->exit = true; + } +} + +// allows the user to add procedurally to the filters from the command line +void Context::addFilter(const char *filter, const char *value) { + setOption(filter, value); +} + +// allows the user to clear all filters from the command line +void Context::clearFilters() { + for (auto &curr: p->filters) + curr.clear(); +} + +// allows the user to override procedurally the bool options from the command line +void Context::setOption(const char *option, bool value) { + setOption(option, value ? "true" : "false"); +} + +// allows the user to override procedurally the int options from the command line +void Context::setOption(const char *option, int value) { + setOption(option, toString(value).c_str()); +} + +// allows the user to override procedurally the string options from the command line +void Context::setOption(const char *option, const char *value) { + auto argv = String("-") + option + "=" + value; + auto lvalue = argv.c_str(); + parseArgs(1, &lvalue); +} + +// users should query this in their main() and exit the program if true +bool Context::shouldExit() { + return p->exit; +} + +void Context::setAsDefaultForAssertsOutOfTestCases() { + detail::g_cs = p; +} + +void Context::setAssertHandler(detail::assert_handler ah) { + p->ah = ah; +} + +void Context::setCout(std::ostream *out) { + p->cout = out; +} + +static class DiscardOStream : public std::ostream { +private: + class : public std::streambuf { + private: + // allowing some buffering decreases the amount of calls to overflow + char buf[1024]; + + protected: + std::streamsize xsputn(const char_type *, std::streamsize count) override { + return count; + } + + int_type overflow(int_type ch) override { + setp(std::begin(buf), std::end(buf)); + return traits_type::not_eof(ch); + } + } discardBuf; + +public: + DiscardOStream() noexcept + : std::ostream(&discardBuf) {} +} discardOut; + +// the main function that does all the filtering and test running +int Context::run() { + using namespace detail; + + // save the old context state in case such was setup - for using asserts out of a testing context + auto old_cs = g_cs; + // this is the current contest + g_cs = p; + is_running_in_test = true; + + g_no_colors = p->no_colors; + p->resetRunData(); + + std::fstream fstr; + if (p->cout == nullptr) { + if (p->quiet) { + p->cout = &discardOut; + } else if (p->out.size()) { + // to a file if specified + fstr.open(p->out.c_str(), std::fstream::out); + p->cout = &fstr; + if (!fstr.is_open()) { + // clang-format off + std::cerr << Color::Cyan << "[doctest] " << Color::None << "Could not open " << p->out << " for writing!" << std::endl; + std::cerr << Color::Cyan << "[doctest] " << Color::None << "Defaulting to std::cout instead" << std::endl; + p->cout = &std::cout; + // clang-format on + } + + } else { +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + // stdout by default + p->cout = &std::cout; +#else // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + return EXIT_FAILURE; +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + } + } + + FatalConditionHandler::allocateAltStackMem(); + + auto cleanup_and_return = [&]() { + FatalConditionHandler::freeAltStackMem(); + + if (fstr.is_open()) + fstr.close(); + + // restore context + g_cs = old_cs; + is_running_in_test = false; + + // we have to free the reporters which were allocated when the run started + for (auto &curr: p->reporters_currently_used) + delete curr; + p->reporters_currently_used.clear(); + + if (p->numTestCasesFailed && !p->no_exitcode) + return EXIT_FAILURE; + return EXIT_SUCCESS; + }; + + // setup default reporter if none is given through the command line + if (p->filters[8].empty()) + p->filters[8].emplace_back("console"); + + // check to see if any of the registered reporters has been selected + for (auto &curr: getReporters()) { + if (matchesAny(curr.first.second.c_str(), p->filters[8], false, p->case_sensitive)) + p->reporters_currently_used.push_back(curr.second(*g_cs)); + } + + // TODO: check if there is nothing in reporters_currently_used + + // prepend all listeners + for (auto &curr: getListeners()) + p->reporters_currently_used.insert(p->reporters_currently_used.begin(), curr.second(*g_cs)); + +#ifdef DOCTEST_PLATFORM_WINDOWS + if (isDebuggerActive() && p->no_debug_output == false) + p->reporters_currently_used.push_back(new DebugOutputWindowReporter(*g_cs)); +#endif // DOCTEST_PLATFORM_WINDOWS + + // handle version, help and no_run + if (p->no_run || p->version || p->help || p->list_reporters) { + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, QueryData()); + + return cleanup_and_return(); + } + + std::vector testArray; + for (auto &curr: getRegisteredTests()) + testArray.push_back(&curr); + p->numTestCases = testArray.size(); + + // sort the collected records + if (!testArray.empty()) { + if (p->order_by.compare("file", true) == 0) { + std::sort(testArray.begin(), testArray.end(), fileOrderComparator); + } else if (p->order_by.compare("suite", true) == 0) { + std::sort(testArray.begin(), testArray.end(), suiteOrderComparator); + } else if (p->order_by.compare("name", true) == 0) { + std::sort(testArray.begin(), testArray.end(), nameOrderComparator); + } else if (p->order_by.compare("rand", true) == 0) { + std::srand(p->rand_seed); + + // random_shuffle implementation + const auto first = testArray.data(); + for (size_t i = testArray.size() - 1; i > 0; --i) { + // NOLINTNEXTLINE(cert-msc30-c, cert-msc50-cpp, concurrency-mt-unsafe, misc-predictable-rand) + const int idxToSwap = static_cast(std::rand() % (i + 1)); + + const auto temp = first[i]; + + first[i] = first[idxToSwap]; + first[idxToSwap] = temp; + } + } else if (p->order_by.compare("none", true) == 0) { + // means no sorting - beneficial for death tests which call into the executable + // with a specific test case in mind - we don't want to slow down the startup times + } + } + + std::set testSuitesPassingFilt; + + const bool query_mode = p->count || p->list_test_cases || p->list_test_suites; + std::vector queryResults; + + if (!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_start, DOCTEST_EMPTY); + + // invoke the registered functions if they match the filter criteria (or just count them) + for (auto &curr: testArray) { + const auto &tc = *curr; + + bool skip_me = false; + if (tc.m_skip && !p->no_skip) + skip_me = true; + + if (!matchesAny(tc.m_file.c_str(), p->filters[0], true, p->case_sensitive)) + skip_me = true; + if (matchesAny(tc.m_file.c_str(), p->filters[1], false, p->case_sensitive)) + skip_me = true; + if (!matchesAny(tc.m_test_suite, p->filters[2], true, p->case_sensitive)) + skip_me = true; + if (matchesAny(tc.m_test_suite, p->filters[3], false, p->case_sensitive)) + skip_me = true; + if (!matchesAny(tc.m_name, p->filters[4], true, p->case_sensitive)) + skip_me = true; + if (matchesAny(tc.m_name, p->filters[5], false, p->case_sensitive)) + skip_me = true; + + if (!skip_me) + p->numTestCasesPassingFilters++; + + // skip the test if it is not in the execution range + if ((p->last < p->numTestCasesPassingFilters && p->first <= p->last) || + (p->first > p->numTestCasesPassingFilters)) + skip_me = true; + + if (skip_me) { + if (!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_skipped, tc); + continue; + } + + // do not execute the test if we are to only count the number of filter passing tests + if (p->count) + continue; + + // print the name of the test and don't execute it + if (p->list_test_cases) { + queryResults.push_back(&tc); + continue; + } + + // print the name of the test suite if not done already and don't execute it + if (p->list_test_suites) { + if ((testSuitesPassingFilt.count(tc.m_test_suite) == 0) && tc.m_test_suite[0] != '\0') { + queryResults.push_back(&tc); + testSuitesPassingFilt.insert(tc.m_test_suite); + p->numTestSuitesPassingFilters++; + } + continue; + } + + // execute the test if it passes all the filtering + { + p->currentTest = &tc; + + p->failure_flags = TestCaseFailureReason::None; + p->seconds = 0; + + // reset atomic counters + p->numAssertsFailedCurrentTest_atomic = 0; + p->numAssertsCurrentTest_atomic = 0; + + p->traversal.resetForTestCase(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_start, tc); + + p->timer.start(); + + bool run_test = true; + + do { // NOLINT(cppcoreguidelines-avoid-do-while) + // Reset per-run traversal data while keeping the current decision path prefix. + p->traversal.resetForRun(); + + p->shouldLogCurrentException = true; + + // reset stuff for logging with INFO() + p->stringifiedContexts.clear(); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + // MSVC 2015 diagnoses fatalConditionHandler as unused (because reset() is a + // static method) + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4101) // unreferenced local variable + const FatalConditionHandler fatalConditionHandler; // Handle signals + static_cast(fatalConditionHandler); + // execute the test + tc.m_test(); + FatalConditionHandler::reset(); + DOCTEST_MSVC_SUPPRESS_WARNING_POP +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + } catch (const TestFailureException &) { + p->failure_flags |= TestCaseFailureReason::AssertFailure; + } catch (...) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {translateActiveException(), false}); + p->failure_flags |= TestCaseFailureReason::Exception; + } +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + + // exit this loop if enough assertions have failed - even if there are more subcases + if (p->abort_after > 0 && + p->numAssertsFailed + p->numAssertsFailedCurrentTest_atomic >= p->abort_after) { + run_test = false; + p->failure_flags |= TestCaseFailureReason::TooManyFailedAsserts; + } + + const bool has_next_path = run_test ? p->traversal.advance() : false; + + if (has_next_path && run_test) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_reenter, tc); + if (!has_next_path) + run_test = false; + } while (run_test); + + p->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + p->currentTest = nullptr; + + // stop executing tests if enough assertions have failed + if (p->abort_after > 0 && p->numAssertsFailed >= p->abort_after) + break; + } + } + + if (!query_mode) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } else { + QueryData qdata; + qdata.run_stats = g_cs; + qdata.data = queryResults.data(); + qdata.num_data = static_cast(queryResults.size()); + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, qdata); + } + + return cleanup_and_return(); +} + +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_CONTEXT_SCOPE +#define DOCTEST_PARTS_PRIVATE_CONTEXT_SCOPE + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { +extern DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_CONTEXT_SCOPE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +DOCTEST_DEFINE_INTERFACE(IContextScope) + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { +DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() + +ContextScopeBase::ContextScopeBase() { + g_infoContexts.push_back(this); +} + +ContextScopeBase::ContextScopeBase(ContextScopeBase &&other) noexcept { + if (other.need_to_destroy) { + other.destroy(); + } + other.need_to_destroy = false; + g_infoContexts.push_back(this); +} + +// destroy cannot be inlined into the destructor because that would mean calling stringify after +// ContextScope has been destroyed (base class destructors run after derived class destructors). +// Instead, ContextScope calls this method directly from its destructor. +void ContextScopeBase::destroy() { + if (detail::has_uncaught_exceptions()) { + std::ostringstream s; + this->stringify(&s); + g_cs->stringifiedContexts.emplace_back(s.str().c_str()); + } + g_infoContexts.pop_back(); +} + +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +ContextState *g_cs = nullptr; +DOCTEST_THREAD_LOCAL bool g_no_colors; + +void ContextState::resetRunData() { + numTestCases = 0; + numTestCasesPassingFilters = 0; + numTestSuitesPassingFilters = 0; + numTestCasesFailed = 0; + numAsserts = 0; + numAssertsFailed = 0; + numAssertsCurrentTest = 0; + numAssertsFailedCurrentTest = 0; +} + +void ContextState::finalizeTestCaseData() { + seconds = timer.getElapsedSeconds(); + + // update the non-atomic counters + numAsserts += numAssertsCurrentTest_atomic; + numAssertsFailed += numAssertsFailedCurrentTest_atomic; + numAssertsCurrentTest = numAssertsCurrentTest_atomic; + numAssertsFailedCurrentTest = numAssertsFailedCurrentTest_atomic; + + if (numAssertsFailedCurrentTest) + failure_flags |= TestCaseFailureReason::AssertFailure; + + if (Approx(currentTest->m_timeout).epsilon(DBL_EPSILON) != 0 && + Approx(seconds).epsilon(DBL_EPSILON) > currentTest->m_timeout) + failure_flags |= TestCaseFailureReason::Timeout; + + if (currentTest->m_should_fail) { + if (failure_flags) { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedAndDid; + } else { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedButDidnt; + } + } else if (failure_flags && currentTest->m_may_fail) { + failure_flags |= TestCaseFailureReason::CouldHaveFailedAndDid; + } else if (currentTest->m_expected_failures > 0) { + if (numAssertsFailedCurrentTest == currentTest->m_expected_failures) { + failure_flags |= TestCaseFailureReason::FailedExactlyNumTimes; + } else { + failure_flags |= TestCaseFailureReason::DidntFailExactlyNumTimes; + } + } + + const bool ok_to_fail = (TestCaseFailureReason::ShouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::CouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::FailedExactlyNumTimes & failure_flags); + + // if any subcase has failed - the whole test case has failed + testCaseSuccess = !(failure_flags && !ok_to_fail); + if (!testCaseSuccess) + numTestCasesFailed++; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { +#ifdef DOCTEST_IS_DEBUGGER_ACTIVE +bool isDebuggerActive() { + return DOCTEST_IS_DEBUGGER_ACTIVE(); +} +#else // DOCTEST_IS_DEBUGGER_ACTIVE +#ifdef DOCTEST_PLATFORM_LINUX +class ErrnoGuard { +public: + ErrnoGuard() + : m_oldErrno(errno) {} + ~ErrnoGuard() { + errno = m_oldErrno; + } + +private: + int m_oldErrno; +}; +// See the comments in Catch2 for the reasoning behind this implementation: +// https://github.com/catchorg/Catch2/blob/v2.13.1/include/internal/catch_debugger.cpp#L79-L102 +bool isDebuggerActive() { + const ErrnoGuard guard; + std::ifstream in("/proc/self/status"); + for (std::string line; std::getline(in, line);) { + static const int PREFIX_LEN = 11; + if (line.compare(0, PREFIX_LEN, "TracerPid:\t") == 0) { + return line.length() > PREFIX_LEN && line[PREFIX_LEN] != '0'; + } + } + return false; +} +#elif defined(DOCTEST_PLATFORM_MAC) +// The following function is taken directly from the following technical note: +// https://developer.apple.com/library/archive/qa/qa1361/_index.html +// Returns true if the current process is being debugged (either +// running under the debugger or has a debugger attached post facto). +bool isDebuggerActive() { + int mib[4]; + kinfo_proc info; + size_t size; + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + info.kp_proc.p_flag = 0; + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + // Call sysctl. + size = sizeof(info); + if (sysctl(mib, DOCTEST_COUNTOF(mib), &info, &size, nullptr, 0) != 0) { + std::cerr << "\nCall to sysctl failed - unable to determine if debugger is active **\n"; + return false; + } + // We're being debugged if the P_TRACED flag is set. + return ((info.kp_proc.p_flag & P_TRACED) != 0); +} +#elif DOCTEST_MSVC || defined(__MINGW32__) || defined(__MINGW64__) +bool isDebuggerActive() { + return ::IsDebuggerPresent() != 0; +} +#else +bool isDebuggerActive() { + return false; +} +#endif // Platform +#endif // DOCTEST_IS_DEBUGGER_ACTIVE +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +DOCTEST_DEFINE_INTERFACE(IExceptionTranslator) + +void registerExceptionTranslatorImpl(const IExceptionTranslator *et) noexcept { + if (std::find(getExceptionTranslators().begin(), getExceptionTranslators().end(), et) == + getExceptionTranslators().end()) + getExceptionTranslators().push_back(et); +} + +std::vector &getExceptionTranslators() noexcept { + static std::vector data; + return data; +} + +String translateActiveException() noexcept { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + String res; + auto &translators = getExceptionTranslators(); + for (auto &curr: translators) + if (curr->translate(res)) + return res; + // clang-format off + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wcatch-value") + try { + throw; + } catch (std::exception &ex) { + return ex.what(); + } catch (std::string &msg) { + return msg.c_str(); + } catch (const char *msg) { + return msg? msg : "(nullptr)"; + } catch (std::nullptr_t) { + return "(nullptr)"; + } catch (...) { + return "unknown exception"; + } + DOCTEST_GCC_SUPPRESS_WARNING_POP + // clang-format on +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + return ""; +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +bool checkIfShouldThrow(assertType::Enum at) { + if (at & assertType::is_require) + return true; + + if ((at & assertType::is_check) && getContextOptions()->abort_after > 0 && + (g_cs->numAssertsFailed + g_cs->numAssertsFailedCurrentTest_atomic) >= getContextOptions()->abort_after) + return true; + + return false; +} + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + +bool has_uncaught_exceptions() { +// Derived from https://github.com/uxlfoundation/oneTBB/blob/v2023.0.0/include/oneapi/tbb/detail/_config.h#L342 +#if (defined(_MSC_VER) && _MSC_VER >= 1900) || \ + (defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && \ + (!defined(_LIBCPP_VERSION) || !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || \ + __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200)) + return (std::uncaught_exceptions() > 0); +#else + return std::uncaught_exception(); +#endif +} + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +DOCTEST_NORETURN void throwException() { + g_cs->shouldLogCurrentException = false; + throw TestFailureException(); // NOLINT(hicpp-exception-baseclass) +} +#else // DOCTEST_CONFIG_NO_EXCEPTIONS +void throwException() {} +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +int wildcmp(const char *str, const char *wild, bool caseSensitive) { + const char *cp = str; + const char *mp = wild; + + while ((*str) && (*wild != '*')) { + if ((caseSensitive ? (*wild != *str) : (tolower(*wild) != tolower(*str))) && (*wild != '?')) { + return 0; + } + wild++; + str++; + } + + while (*str) { + if (*wild == '*') { + if (!*++wild) { + return 1; + } + mp = wild; + cp = str + 1; + } else if ((caseSensitive ? (*wild == *str) : (tolower(*wild) == tolower(*str))) || (*wild == '?')) { + wild++; + str++; + } else { + wild = mp; + str = cp++; + } + } + + while (*wild == '*') { + wild++; + } + return !*wild; +} + +bool matchesAny(const char *name, const std::vector &filters, bool matchEmpty, bool caseSensitive) { + if (filters.empty() && matchEmpty) + return true; + for (auto &curr: filters) + if (wildcmp(name, curr.c_str(), caseSensitive)) + return true; + return false; +} + +} // namespace detail +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) // 'function' : must be 'attribute' - see issue #182 +int main(int argc, char **argv) { + return doctest::Context(argc, argv).run(); +} +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +Approx::Approx(double value) + : m_epsilon(static_cast(std::numeric_limits::epsilon()) * 100), m_scale(1.0), m_value(value) {} + +Approx Approx::operator()(double value) const { + Approx approx(value); + approx.epsilon(m_epsilon); + approx.scale(m_scale); + return approx; +} + +Approx &Approx::epsilon(double newEpsilon) { + m_epsilon = newEpsilon; + return *this; +} +Approx &Approx::scale(double newScale) { + m_scale = newScale; + return *this; +} + +bool operator==(double lhs, const Approx &rhs) { + // Thanks to Richard Harris for his help refining this formula + return std::fabs(lhs - rhs.m_value) < + rhs.m_epsilon * (rhs.m_scale + std::max(std::fabs(lhs), std::fabs(rhs.m_value))); +} + +bool operator==(const Approx &lhs, double rhs) { + return operator==(rhs, lhs); +} + +bool operator!=(double lhs, const Approx &rhs) { + return !operator==(lhs, rhs); +} + +bool operator!=(const Approx &lhs, double rhs) { + return !operator==(rhs, lhs); +} + +bool operator<=(double lhs, const Approx &rhs) { + return lhs < rhs.m_value || lhs == rhs; +} + +bool operator<=(const Approx &lhs, double rhs) { + return lhs.m_value < rhs || lhs == rhs; +} + +bool operator>=(double lhs, const Approx &rhs) { + return lhs > rhs.m_value || lhs == rhs; +} + +bool operator>=(const Approx &lhs, double rhs) { + return lhs.m_value > rhs || lhs == rhs; +} + +bool operator<(double lhs, const Approx &rhs) { + return lhs < rhs.m_value && lhs != rhs; +} + +bool operator<(const Approx &lhs, double rhs) { + return lhs.m_value < rhs && lhs != rhs; +} + +bool operator>(double lhs, const Approx &rhs) { + return lhs > rhs.m_value && lhs != rhs; +} + +bool operator>(const Approx &lhs, double rhs) { + return lhs.m_value > rhs && lhs != rhs; +} + +String toString(const Approx &in) { + return "Approx( " + doctest::toString(in.m_value) + " )"; +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +Contains::Contains(const String &str) + : string(str) {} + +bool Contains::checkWith(const String &other) const { + return strstr(other.c_str(), string.c_str()) != nullptr; +} + +String toString(const Contains &in) { + return "Contains( " + in.string + " )"; +} + +bool operator==(const String &lhs, const Contains &rhs) { + return rhs.checkWith(lhs); +} + +bool operator==(const Contains &lhs, const String &rhs) { + return lhs.checkWith(rhs); +} + +bool operator!=(const String &lhs, const Contains &rhs) { + return !rhs.checkWith(lhs); +} + +bool operator!=(const Contains &lhs, const String &rhs) { + return !lhs.checkWith(rhs); +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4738) +template +IsNaN::operator bool() const { + return std::isnan(value) ^ flipped; +} +DOCTEST_MSVC_SUPPRESS_WARNING_POP +template struct DOCTEST_INTERFACE_DEF IsNaN; +template struct DOCTEST_INTERFACE_DEF IsNaN; +template struct DOCTEST_INTERFACE_DEF IsNaN; + +template +String toString(IsNaN in) { + return String(in.flipped ? "! " : "") + "IsNaN( " + doctest::toString(in.value) + " )"; +} + +String toString(IsNaN in) { + return toString(in); +} + +String toString(IsNaN in) { + return toString(in); +} + +String toString(IsNaN in) { + return toString(in); +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_OPTIONS_FILE_PREFIX_SEPARATOR +#define DOCTEST_CONFIG_OPTIONS_FILE_PREFIX_SEPARATOR ':' +#endif + +namespace doctest { + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +// depending on the current options this will remove the path of filenames +const char *skipPathFromFilename(const char *file) { +#ifndef DOCTEST_CONFIG_DISABLE + if (getContextOptions()->no_path_in_filenames) { + auto back = std::strrchr(file, '\\'); + auto forward = std::strrchr(file, '/'); + if (back || forward) { + if (back > forward) + forward = back; + return forward + 1; + } + } else { + const auto prefixes = getContextOptions()->strip_file_prefixes; + const char separator = DOCTEST_CONFIG_OPTIONS_FILE_PREFIX_SEPARATOR; + String::size_type longest_match = 0U; + for (String::size_type pos = 0U; pos < prefixes.size(); ++pos) { + const auto prefix_start = pos; + pos = std::min(prefixes.find(separator, prefix_start), prefixes.size()); + + const auto prefix_size = pos - prefix_start; + if (prefix_size > longest_match) { + // TODO under DOCTEST_MSVC: does the comparison need strnicmp() to work with drive + // letter capitalization? + if (0 == std::strncmp(prefixes.c_str() + prefix_start, file, prefix_size)) { + longest_match = prefix_size; + } + } + } + return &file[longest_match]; + } +#endif // DOCTEST_CONFIG_DISABLE + return file; +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { +#ifdef DOCTEST_CONFIG_DISABLE + +DOCTEST_DEFINE_INTERFACE(IReporter) + +int IReporter::get_num_active_contexts() { + return 0; +} + +const IContextScope *const *IReporter::get_active_contexts() { + return nullptr; +} + +int IReporter::get_num_stringified_contexts() { + return 0; +} + +const String *IReporter::get_stringified_contexts() { + return nullptr; +} + +int registerReporter(const char *, int, IReporter *) { + return 0; +} + +#else + +namespace detail { +reporterMap &getReporters() noexcept { + static reporterMap data; + return data; +} + +reporterMap &getListeners() noexcept { + static reporterMap data; + return data; +} +} // namespace detail + +DOCTEST_DEFINE_INTERFACE(IReporter) + +int IReporter::get_num_active_contexts() { + return static_cast(detail::g_infoContexts.size()); +} + +const IContextScope *const *IReporter::get_active_contexts() { + return get_num_active_contexts() ? detail::g_infoContexts.data() : nullptr; +} + +int IReporter::get_num_stringified_contexts() { + return static_cast(detail::g_cs->stringifiedContexts.size()); +} + +const String *IReporter::get_stringified_contexts() { + return get_num_stringified_contexts() ? detail::g_cs->stringifiedContexts.data() : nullptr; +} + +namespace detail { +void registerReporterImpl(const char *name, int priority, reporterCreatorFunc c, bool isReporter) noexcept { + if (isReporter) + getReporters().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + else + getListeners().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); +} +} // namespace detail + +#endif // DOCTEST_CONFIG_DISABLE +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +void fulltext_log_assert_to_stream(std::ostream &s, const AssertData &rb) { + // clang-format off + if ((rb.m_at & (assertType::is_throws_as | assertType::is_throws_with)) == 0) + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << " ) " << Color::None; + + if (rb.m_at & assertType::is_throws) { + s << (rb.m_threw ? "threw as expected!" : "did NOT throw at all!") << "\n"; + } else if ((rb.m_at & assertType::is_throws_as) && (rb.m_at & assertType::is_throws_with)) { + s << Color::Cyan << assertString(rb.m_at) << "( " + << rb.m_expr << ", \"" << rb.m_exception_string.c_str() << "\", " << rb.m_exception_type + << " ) " << Color::None; + + if (rb.m_threw) { + if (!rb.m_failed) { + s << "threw as expected!\n"; + } else { + s << "threw a DIFFERENT exception! (contents: " << rb.m_exception << ")\n"; + } + } else { + s << "did NOT throw at all!\n"; + } + } else if (rb.m_at & assertType::is_throws_as) { + s << Color::Cyan << assertString(rb.m_at) << "( " + << rb.m_expr << ", " << rb.m_exception_type + << " ) " << Color::None + << (rb.m_threw ? (rb.m_threw_as ? "threw as expected!" : "threw a DIFFERENT exception: ") : "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if (rb.m_at & assertType::is_throws_with) { + s << Color::Cyan << assertString(rb.m_at) << "( " + << rb.m_expr << ", \"" << rb.m_exception_string.c_str() + << "\" ) " << Color::None + << (rb.m_threw ? (!rb.m_failed ? "threw as expected!" : "threw a DIFFERENT exception: ") : "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if (rb.m_at & assertType::is_nothrow) { + s << (rb.m_threw ? "THREW exception: " : "didn't throw!") << Color::Cyan << rb.m_exception << "\n"; + } else { + s << (rb.m_threw ? "THREW exception: " : (!rb.m_failed ? "is correct!\n" : "is NOT correct!\n")); + if (rb.m_threw) + s << rb.m_exception << "\n"; + else + s << " values: " << assertString(rb.m_at) << "( " << rb.m_decomp << " )\n"; + } + // clang-format on +} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +using detail::g_cs; + +Whitespace::Whitespace(int nr) + : nrSpaces(nr) {} + +std::ostream &operator<<(std::ostream &out, const Whitespace &ws) { + if (ws.nrSpaces != 0) + out << std::setw(ws.nrSpaces) << ' '; + return out; +} + +ConsoleReporter::ConsoleReporter(const ContextOptions &co) + : s(*co.cout), opt(co) {} + +ConsoleReporter::ConsoleReporter(const ContextOptions &co, std::ostream &ostr) + : s(ostr), opt(co) {} + +void ConsoleReporter::separator_to_stream() { + s << Color::Yellow + << "===============================================================================" + "\n"; +} + +const char *ConsoleReporter::getSuccessOrFailString(bool success, assertType::Enum at, const char *success_str) { + if (success) + return success_str; + return failureString(at); +} + +Color::Enum ConsoleReporter::getSuccessOrFailColor(bool success, assertType::Enum at) { + return success ? Color::BrightGreen : (at & assertType::is_warn) ? Color::Yellow : Color::Red; +} + +void ConsoleReporter::successOrFailColoredStringToStream(bool success, assertType::Enum at, const char *success_str) { + s << getSuccessOrFailColor(success, at) << getSuccessOrFailString(success, at, success_str) << ": "; +} + +void ConsoleReporter::log_contexts() { + const int num_contexts = get_num_active_contexts(); + if (num_contexts) { + auto contexts = get_active_contexts(); + + s << Color::None << " logged: "; + for (int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << "\n"; + } + } + + s << "\n"; +} + +// this was requested to be made virtual so users could override it +void ConsoleReporter::file_line_to_stream(const char *file, int line, const char *tail) { + s << Color::LightGrey << skipPathFromFilename(file) << (opt.gnu_file_line ? ":" : "(") + << (opt.no_line_numbers ? 0 : line) // 0 or the real num depending on the option + << (opt.gnu_file_line ? ":" : "):") << tail; +} + +void ConsoleReporter::logTestStart() { + if (hasLoggedCurrentTestStart) + return; + + separator_to_stream(); + file_line_to_stream(tc->m_file.c_str(), static_cast(tc->m_line), "\n"); + if (tc->m_description) + s << Color::Yellow << "DESCRIPTION: " << Color::None << tc->m_description << "\n"; + if (tc->m_test_suite && tc->m_test_suite[0] != '\0') + s << Color::Yellow << "TEST SUITE: " << Color::None << tc->m_test_suite << "\n"; + if (strncmp(tc->m_name, " Scenario:", 11) != 0) + s << Color::Yellow << "TEST CASE: "; + s << Color::None << tc->m_name << "\n"; + + for (size_t i = 0; i < currentSubcaseLevel; ++i) { + if (subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + + if (currentSubcaseLevel != subcasesStack.size()) { + s << Color::Yellow << "\nDEEPEST SUBCASE STACK REACHED (DIFFERENT FROM THE CURRENT ONE):\n" << Color::None; + for (size_t i = 0; i < subcasesStack.size(); ++i) { + if (subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + } + + s << "\n"; + + hasLoggedCurrentTestStart = true; +} + +void ConsoleReporter::printVersion() { + if (opt.no_version == false) + s << Color::Cyan << "[doctest] " << Color::None << "doctest version is \"" << DOCTEST_VERSION_STR << "\"\n"; +} + +void ConsoleReporter::printIntro() { + if (opt.no_intro == false) { + printVersion(); + s << Color::Cyan << "[doctest] " << Color::None + << "run with \"--" DOCTEST_OPTIONS_PREFIX_DISPLAY "help\" for options\n"; + } +} + +void ConsoleReporter::printHelp() { + const int sizePrefixDisplay = static_cast(strlen(DOCTEST_OPTIONS_PREFIX_DISPLAY)); + printVersion(); + // clang-format off + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "boolean values: \"1/on/yes/true\" or \"0/off/no/false\"\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filter values: \"str1,str2,str3\" (comma separated strings)\n"; + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filters use wildcards for matching strings\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "something passes a filter if any of the strings in a filter matches\n"; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "ALL FLAGS, OPTIONS AND FILTERS ALSO AVAILABLE WITH A \"" DOCTEST_CONFIG_OPTIONS_PREFIX "\" PREFIX!!!\n"; +#endif + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "Query flags - the program quits after them. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "?, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "help, -" DOCTEST_OPTIONS_PREFIX_DISPLAY "h " + << Whitespace(sizePrefixDisplay * 0) << "prints this message\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "v, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "version " + << Whitespace(sizePrefixDisplay * 1) << "prints the version\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "c, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "count " + << Whitespace(sizePrefixDisplay * 1) << "prints the number of matching tests\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ltc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-cases " + << Whitespace(sizePrefixDisplay * 1) << "lists all matching tests by name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-suites " + << Whitespace(sizePrefixDisplay * 1) << "lists all matching test suites\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-reporters " + << Whitespace(sizePrefixDisplay * 1) << "lists all registered reporters\n\n"; + // ================================================================================== << 79 + s << Color::Cyan << "[doctest] " << Color::None; + s << "The available / options/filters are:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case= " + << Whitespace(sizePrefixDisplay * 1) << "filters tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case-exclude= " + << Whitespace(sizePrefixDisplay * 1) << "filters OUT tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file= " + << Whitespace(sizePrefixDisplay * 1) << "filters tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfe, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file-exclude= " + << Whitespace(sizePrefixDisplay * 1) << "filters OUT tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite= " + << Whitespace(sizePrefixDisplay * 1) << "filters tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tse, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite-exclude= " + << Whitespace(sizePrefixDisplay * 1) << "filters OUT tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase= " + << Whitespace(sizePrefixDisplay * 1) << "filters subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-exclude= " + << Whitespace(sizePrefixDisplay * 1) << "filters OUT subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "r, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "reporters= " + << Whitespace(sizePrefixDisplay * 1) << "reporters to use (console is default)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "o, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "out= " + << Whitespace(sizePrefixDisplay * 1) << "output filename\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ob, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "order-by= " + << Whitespace(sizePrefixDisplay * 1) << "how the tests should be ordered\n"; + s << Whitespace(sizePrefixDisplay * 3) << " - [file/suite/name/rand/none]\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "rs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "rand-seed= " + << Whitespace(sizePrefixDisplay * 1) << "seed for random ordering\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "f, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "first= " + << Whitespace(sizePrefixDisplay * 1) << "the first test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay * 3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "l, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "last= " + << Whitespace(sizePrefixDisplay * 1) << "the last test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay * 3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "aa, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "abort-after= " + << Whitespace(sizePrefixDisplay * 1) << "stop after failed assertions\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "scfl,--" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-filter-levels= " + << Whitespace(sizePrefixDisplay * 1) << "apply filters for the first levels\n"; + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "Bool options - can be used like flags and true is assumed. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "s, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "success= " + << Whitespace(sizePrefixDisplay * 1) << "include successful assertions in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "cs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "case-sensitive= " + << Whitespace(sizePrefixDisplay * 1) << "filters being treated as case sensitive\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "e, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "exit= " + << Whitespace(sizePrefixDisplay * 1) << "exits after the tests finish\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "d, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "duration= " + << Whitespace(sizePrefixDisplay * 1) << "prints the time duration of each test\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "m, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "minimal= " + << Whitespace(sizePrefixDisplay * 1) << "minimal console output (only failures)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "q, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "quiet= " + << Whitespace(sizePrefixDisplay * 1) << "no console output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nt, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-throw= " + << Whitespace(sizePrefixDisplay * 1) << "skips exceptions-related assert checks\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ne, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-exitcode= " + << Whitespace(sizePrefixDisplay * 1) << "returns (or exits) always with success\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-run= " + << Whitespace(sizePrefixDisplay * 1) << "skips all runtime doctest operations\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ni, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-intro= " + << Whitespace(sizePrefixDisplay * 1) << "omit the framework intro in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nv, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-version= " + << Whitespace(sizePrefixDisplay * 1) << "omit the framework version in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-colors= " + << Whitespace(sizePrefixDisplay * 1) << "disables colors in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "fc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "force-colors= " + << Whitespace(sizePrefixDisplay * 1) << "use colors even when not in a tty\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nb, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-breaks= " + << Whitespace(sizePrefixDisplay * 1) << "disables breakpoints in debuggers\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ns, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-skip= " + << Whitespace(sizePrefixDisplay * 1) << "don't skip test cases marked as skip\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "gfl, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "gnu-file-line= " + << Whitespace(sizePrefixDisplay * 1) << ":n: vs (n): for line numbers in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "npf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-path-filenames= " + << Whitespace(sizePrefixDisplay * 1) << "only filenames and no paths in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfp, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "strip-file-prefixes= " + << Whitespace(sizePrefixDisplay * 1) << "whenever file paths start with this prefix, remove it from the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nln, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-line-numbers= " + << Whitespace(sizePrefixDisplay * 1) << "0 instead of real line numbers in output\n"; + // ================================================================================== << 79 + // clang-format on + + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "for more information visit the project documentation\n\n"; +} + +void ConsoleReporter::printRegisteredReporters() { + printVersion(); + auto printReporters = [this](const detail::reporterMap &reporters, const char *type) { + if (!reporters.empty()) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all registered " << type << "\n"; + for (auto &curr: reporters) + s << "priority: " << std::setw(5) << curr.first.first << " name: " << curr.first.second << "\n"; + } + }; + printReporters(detail::getListeners(), "listeners"); + printReporters(detail::getReporters(), "reporters"); +} + +void ConsoleReporter::report_query(const QueryData &in) { + if (opt.version) { + printVersion(); + } else if (opt.help) { + printHelp(); + } else if (opt.list_reporters) { + printRegisteredReporters(); + } else if (opt.count || opt.list_test_cases) { + if (opt.list_test_cases) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all test case names\n"; + separator_to_stream(); + } + + for (unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_name << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " << g_cs->numTestCasesPassingFilters << "\n"; + + } else if (opt.list_test_suites) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all test suites\n"; + separator_to_stream(); + + for (unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_test_suite << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " << g_cs->numTestCasesPassingFilters << "\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "test suites with unskipped test cases passing the current filters: " << g_cs->numTestSuitesPassingFilters + << "\n"; + } +} + +void ConsoleReporter::test_run_start() { + if (!opt.minimal) + printIntro(); +} + +void ConsoleReporter::test_run_end(const TestRunStats &p) { + if (opt.minimal && p.numTestCasesFailed == 0) + return; + + separator_to_stream(); + s << std::dec; + + auto totwidth = static_cast(std::ceil( + log10(static_cast(std::max(p.numTestCasesPassingFilters, static_cast(p.numAsserts))) + 1) + )); + auto passwidth = static_cast(std::ceil(log10( + static_cast(std::max( + p.numTestCasesPassingFilters - p.numTestCasesFailed, + static_cast(p.numAsserts - p.numAssertsFailed) + )) + + 1 + ))); + auto failwidth = static_cast(std::ceil( + log10(static_cast(std::max(p.numTestCasesFailed, static_cast(p.numAssertsFailed))) + 1) + )); + const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0; + s << Color::Cyan << "[doctest] " << Color::None << "test cases: " << std::setw(totwidth) + << p.numTestCasesPassingFilters << " | " + << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None : Color::Green) << std::setw(passwidth) + << p.numTestCasesPassingFilters - p.numTestCasesFailed << " passed" << Color::None << " | " + << (p.numTestCasesFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth) << p.numTestCasesFailed + << " failed" << Color::None << " |"; + if (opt.no_skipped_summary == false) { + const unsigned int numSkipped = p.numTestCases - p.numTestCasesPassingFilters; + s << " " << (numSkipped == 0 ? Color::None : Color::Yellow) << numSkipped << " skipped" << Color::None; + } + s << "\n"; + s << Color::Cyan << "[doctest] " << Color::None << "assertions: " << std::setw(totwidth) << p.numAsserts << " | " + << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green) << std::setw(passwidth) + << (p.numAsserts - p.numAssertsFailed) << " passed" << Color::None << " | " + << (p.numAssertsFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth) << p.numAssertsFailed << " failed" + << Color::None << " |\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green) + << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl; +} + +void ConsoleReporter::test_case_start(const TestCaseData &in) { + DOCTEST_LOCK_MUTEX(mutex) + hasLoggedCurrentTestStart = false; + tc = ∈ + subcasesStack.clear(); + currentSubcaseLevel = 0; +} + +void ConsoleReporter::test_case_reenter(const TestCaseData &) { + DOCTEST_LOCK_MUTEX(mutex) + subcasesStack.clear(); +} + +void ConsoleReporter::test_case_end(const CurrentTestCaseStats &st) { + DOCTEST_LOCK_MUTEX(mutex) + if (tc->m_no_output) + return; + + // log the preamble of the test case only if there is something + // else to print - something other than that an assert has failed + if (opt.duration || + (st.failure_flags && st.failure_flags != static_cast(TestCaseFailureReason::AssertFailure))) + logTestStart(); + + if (opt.duration) + s << Color::None << std::setprecision(6) << std::fixed << st.seconds << " s: " << tc->m_name << "\n"; + + if (st.failure_flags & TestCaseFailureReason::Timeout) + s << Color::Red << "Test case exceeded time limit of " << std::setprecision(6) << std::fixed << tc->m_timeout + << "!\n"; + + if (st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { + s << Color::Red << "Should have failed but didn't! Marking it as failed!\n"; + } else if (st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) { + s << Color::Yellow << "Failed as expected so marking it as not failed\n"; + } else if (st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) { + s << Color::Yellow << "Allowed to fail so marking it as not failed\n"; + } else if (st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { + s << Color::Red << "Didn't fail exactly " << tc->m_expected_failures << " times so marking it as failed!\n"; + } else if (st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) { + s << Color::Yellow << "Failed exactly " << tc->m_expected_failures + << " times as expected so marking it as not failed!\n"; + } + if (st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { + s << Color::Red << "Aborting - too many failed asserts!\n"; + } + s << Color::None; +} + +void ConsoleReporter::test_case_exception(const TestCaseException &e) { + DOCTEST_LOCK_MUTEX(mutex) + if (tc->m_no_output) + return; + + logTestStart(); + + file_line_to_stream(tc->m_file.c_str(), static_cast(tc->m_line), " "); + successOrFailColoredStringToStream(false, e.is_crash ? assertType::is_require : assertType::is_check); + s << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: "); + s << Color::Cyan << e.error_string << "\n"; + + const int num_stringified_contexts = get_num_stringified_contexts(); + if (num_stringified_contexts) { + auto stringified_contexts = get_stringified_contexts(); + s << Color::None << " logged: "; + for (int i = num_stringified_contexts; i > 0; --i) { + s << (i == num_stringified_contexts ? "" : " ") << stringified_contexts[i - 1] << "\n"; + } + } + s << "\n" << Color::None; +} + +void ConsoleReporter::subcase_start(const SubcaseSignature &subc) { + DOCTEST_LOCK_MUTEX(mutex) + subcasesStack.push_back(subc); + ++currentSubcaseLevel; + hasLoggedCurrentTestStart = false; +} + +void ConsoleReporter::subcase_end() { + DOCTEST_LOCK_MUTEX(mutex) + --currentSubcaseLevel; + hasLoggedCurrentTestStart = false; +} + +void ConsoleReporter::log_assert(const AssertData &rb) { + if ((!rb.m_failed && !opt.success) || tc->m_no_output) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + logTestStart(); + + file_line_to_stream(rb.m_file, rb.m_line, " "); + successOrFailColoredStringToStream(!rb.m_failed, rb.m_at); + + fulltext_log_assert_to_stream(s, rb); + + log_contexts(); +} + +void ConsoleReporter::log_message(const MessageData &mb) { + if (tc->m_no_output) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + logTestStart(); + + file_line_to_stream(mb.m_file, mb.m_line, " "); + s << getSuccessOrFailColor(false, mb.m_severity) + << getSuccessOrFailString(mb.m_severity & assertType::is_warn, mb.m_severity, "MESSAGE") << ": "; + s << Color::None << mb.m_string << "\n"; + log_contexts(); +} + +void ConsoleReporter::test_case_skipped(const TestCaseData &) {} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_OUTPUT_DEBUG_STRING(text) ::OutputDebugStringA(text) +#endif // Platform + +#ifdef DOCTEST_PLATFORM_WINDOWS + +DOCTEST_THREAD_LOCAL std::ostringstream DebugOutputWindowReporter::oss; + +DebugOutputWindowReporter::DebugOutputWindowReporter(const ContextOptions &co) + : ConsoleReporter(co, oss) {} + +#define DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(func, type, arg) \ + void DebugOutputWindowReporter::func(type arg) { \ + using detail::g_no_colors; \ + bool with_col = g_no_colors; \ + g_no_colors = false; \ + ConsoleReporter::func(arg); \ + if (oss.tellp() != std::streampos{}) { \ + DOCTEST_OUTPUT_DEBUG_STRING(oss.str().c_str()); \ + oss.str(""); \ + } \ + g_no_colors = with_col; \ + } + +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_start, DOCTEST_EMPTY, DOCTEST_EMPTY) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_end, const TestRunStats &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_start, const TestCaseData &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_reenter, const TestCaseData &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_end, const CurrentTestCaseStats &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_exception, const TestCaseException &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_start, const SubcaseSignature &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_end, DOCTEST_EMPTY, DOCTEST_EMPTY) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_assert, const AssertData &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_message, const MessageData &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_skipped, const TestCaseData &, in) + +#endif // DOCTEST_PLATFORM_WINDOWS + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +std::string JUnitReporter::JUnitTestCaseData::getCurrentTimestamp() { + // Beware, this is not reentrant because of backward compatibility issues + // Also, UTC only, again because of backward compatibility (%z is C++11) + time_t rawtime{}; + static_cast(std::time(&rawtime)); + const auto timeStampSize = sizeof("2017-01-16T17:06:45Z"); + + std::tm timeInfo; +#if defined(DOCTEST_PLATFORM_WINDOWS) + gmtime_s(&timeInfo, &rawtime); +#elif defined(__STDC_LIB_EXT1__) + gmtime_s(&rawtime, &timeInfo); +#else // DOCTEST_PLATFORM_WINDOWS + gmtime_r(&rawtime, &timeInfo); +#endif // DOCTEST_PLATFORM_WINDOWS + + char timeStamp[timeStampSize]; + const char *const fmt = "%Y-%m-%dT%H:%M:%SZ"; + + static_cast(std::strftime(timeStamp, timeStampSize, fmt, &timeInfo)); + return std::string(timeStamp); +} + +JUnitReporter::JUnitTestCaseData::JUnitTestMessage::JUnitTestMessage( + const std::string &_message, const std::string &_type, const std::string &_details +) + : message(_message), type(_type), details(_details) {} + +JUnitReporter::JUnitTestCaseData::JUnitTestMessage::JUnitTestMessage( + const std::string &_message, const std::string &_details +) + : message(_message), type(), details(_details) {} + +JUnitReporter::JUnitTestCaseData::JUnitTestCase::JUnitTestCase(const std::string &_classname, const std::string &_name) + : classname(_classname), name(_name), time(0), failures(), errors() {} + +void JUnitReporter::JUnitTestCaseData::add(const std::string &classname, const std::string &name) { + testcases.emplace_back(classname, name); +} + +void JUnitReporter::JUnitTestCaseData::appendSubcaseNamesToLastTestcase(std::vector nameStack) { + for (auto &curr: nameStack) + if (curr.size()) + testcases.back().name += std::string("/") + curr.c_str(); +} + +void JUnitReporter::JUnitTestCaseData::addTime(double time) { + if (time < 1e-4) + time = 0; + testcases.back().time = time; + totalSeconds += time; +} + +void JUnitReporter::JUnitTestCaseData::addFailure( + const std::string &message, const std::string &type, const std::string &details +) { + testcases.back().failures.emplace_back(message, type, details); + ++totalFailures; +} + +void JUnitReporter::JUnitTestCaseData::addError(const std::string &message, const std::string &details) { + testcases.back().errors.emplace_back(message, details); + ++totalErrors; +} + +JUnitReporter::JUnitReporter(const ContextOptions &co) + : xml(*co.cout), opt(co) {} + +unsigned JUnitReporter::line(unsigned l) const { + return opt.no_line_numbers ? 0 : l; +} + +void JUnitReporter::report_query(const QueryData &) { + xml.writeDeclaration(); +} + +void JUnitReporter::test_run_start() { + xml.writeDeclaration(); +} + +void JUnitReporter::test_run_end(const TestRunStats &p) { + // remove .exe extension - mainly to have the same output on UNIX and Windows + // NOLINTNEXTLINE(misc-const-correctness) + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if (binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + + xml.startElement("testsuites"); + xml.startElement("testsuite") + .writeAttribute("name", binary_name) + .writeAttribute("errors", testCaseData.totalErrors) + .writeAttribute("failures", testCaseData.totalFailures) + .writeAttribute("tests", p.numAsserts); + if (opt.no_time_in_output == false) { + xml.writeAttribute("time", testCaseData.totalSeconds); + xml.writeAttribute("timestamp", JUnitTestCaseData::getCurrentTimestamp()); + } + if (opt.no_version == false) + xml.writeAttribute("doctest_version", DOCTEST_VERSION_STR); + + for (const auto &testCase: testCaseData.testcases) { + xml.startElement("testcase") + .writeAttribute("classname", testCase.classname) + .writeAttribute("name", testCase.name); + if (opt.no_time_in_output == false) + xml.writeAttribute("time", testCase.time); + // This is not ideal, but it should be enough to mimic gtest's junit output. + xml.writeAttribute("status", "run"); + + for (const auto &failure: testCase.failures) { + xml.scopedElement("failure") + .writeAttribute("message", failure.message) + .writeAttribute("type", failure.type) + .writeText(failure.details, false); + } + + for (const auto &error: testCase.errors) { + xml.scopedElement("error").writeAttribute("message", error.message).writeText(error.details); + } + + xml.endElement(); + } + xml.endElement(); + xml.endElement(); +} + +void JUnitReporter::test_case_start(const TestCaseData &in) { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + timer.start(); + tc = ∈ +} + +void JUnitReporter::test_case_reenter(const TestCaseData &in) { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + + timer.start(); + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + tc = ∈ +} + +void JUnitReporter::test_case_end(const CurrentTestCaseStats &st) { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + + if (st.failure_flags & TestCaseFailureReason::Timeout) { + auto *stream = detail::tlssPush(); + *stream << "Test case exceeded time limit of " << std::setprecision(6) << std::fixed << tc->m_timeout; + testCaseData.addError("timeout", detail::tlssPop().c_str()); + } + + if (st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { + testCaseData.addError("should_fail", "Should have failed, but didn't"); + } else if (st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { + auto *stream = detail::tlssPush(); + *stream << "Should have failed exactly " << tc->m_expected_failures << " times, but didn't"; + testCaseData.addError("should_fail", detail::tlssPop().c_str()); + } + + if (st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { + testCaseData.addError("abort_after", "Too many failed asserts"); + } + + tc = nullptr; +} + +void JUnitReporter::test_case_exception(const TestCaseException &e) { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.addError("exception", e.error_string.c_str()); +} + +void JUnitReporter::subcase_start(const SubcaseSignature &in) { + DOCTEST_LOCK_MUTEX(mutex) + deepestSubcaseStackNames.push_back(in.m_name); +} + +void JUnitReporter::subcase_end() {} + +void JUnitReporter::log_assert(const AssertData &rb) { + if (!rb.m_failed) // report only failures & ignore the `success` option + return; + + DOCTEST_LOCK_MUTEX(mutex) + + std::ostringstream os; + os << skipPathFromFilename(rb.m_file) << (opt.gnu_file_line ? ":" : "(") << line(rb.m_line) + << (opt.gnu_file_line ? ":" : "):") << std::endl; + + fulltext_log_assert_to_stream(os, rb); + log_contexts(os); + testCaseData.addFailure(rb.m_decomp.c_str(), assertString(rb.m_at), os.str()); +} + +void JUnitReporter::log_message(const MessageData &mb) { + if (mb.m_severity & assertType::is_warn) // report only failures + return; + + DOCTEST_LOCK_MUTEX(mutex) + + std::ostringstream os; + os << skipPathFromFilename(mb.m_file) << (opt.gnu_file_line ? ":" : "(") << line(mb.m_line) + << (opt.gnu_file_line ? ":" : "):") << std::endl; + + os << mb.m_string.c_str() << "\n"; + log_contexts(os); + + testCaseData.addFailure( + mb.m_string.c_str(), mb.m_severity & assertType::is_check ? "FAIL_CHECK" : "FAIL", os.str() + ); +} + +void JUnitReporter::test_case_skipped(const TestCaseData &) {} + +void JUnitReporter::log_contexts(std::ostringstream &s) { + const int num_contexts = get_num_active_contexts(); + if (num_contexts) { + auto contexts = get_active_contexts(); + + s << " logged: "; + for (int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << std::endl; + } + } +} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +XmlReporter::XmlReporter(const ContextOptions &co) + : xml(*co.cout), opt(co) {} + +void XmlReporter::log_contexts() { + const int num_contexts = get_num_active_contexts(); + if (num_contexts) { + auto contexts = get_active_contexts(); + std::stringstream ss; + for (int i = 0; i < num_contexts; ++i) { + contexts[i]->stringify(&ss); + xml.scopedElement("Info").writeText(ss.str()); + ss.str(""); + } + } +} + +unsigned XmlReporter::line(unsigned l) const { + return opt.no_line_numbers ? 0 : l; +} + +void XmlReporter::test_case_start_impl(const TestCaseData &in) { + bool open_ts_tag = false; + if (tc != nullptr) { // we have already opened a test suite + if (std::strcmp(tc->m_test_suite, in.m_test_suite) != 0) { + xml.endElement(); + open_ts_tag = true; + } + } else { + open_ts_tag = true; // first test case ==> first test suite + } + + if (open_ts_tag) { + xml.startElement("TestSuite"); + xml.writeAttribute("name", in.m_test_suite); + } + + tc = ∈ + xml.startElement("TestCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file.c_str())) + .writeAttribute("line", line(in.m_line)) + .writeAttribute("description", in.m_description); + + if (Approx(in.m_timeout) != 0) + xml.writeAttribute("timeout", in.m_timeout); + if (in.m_may_fail) + xml.writeAttribute("may_fail", true); + if (in.m_should_fail) + xml.writeAttribute("should_fail", true); +} + +void XmlReporter::report_query(const QueryData &in) { + test_run_start(); + if (opt.list_reporters) { + for (auto &curr: detail::getListeners()) + xml.scopedElement("Listener") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + for (auto &curr: detail::getReporters()) + xml.scopedElement("Reporter") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + } else if (opt.count || opt.list_test_cases) { + for (unsigned i = 0; i < in.num_data; ++i) { + xml.scopedElement("TestCase") + .writeAttribute("name", in.data[i]->m_name) + .writeAttribute("testsuite", in.data[i]->m_test_suite) + .writeAttribute("filename", skipPathFromFilename(in.data[i]->m_file.c_str())) + .writeAttribute("line", line(in.data[i]->m_line)) + .writeAttribute("skipped", in.data[i]->m_skip); + } + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + } else if (opt.list_test_suites) { + for (unsigned i = 0; i < in.num_data; ++i) + xml.scopedElement("TestSuite").writeAttribute("name", in.data[i]->m_test_suite); + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + xml.scopedElement("OverallResultsTestSuites") + .writeAttribute("unskipped", in.run_stats->numTestSuitesPassingFilters); + } + xml.endElement(); +} + +void XmlReporter::test_run_start() { + xml.writeDeclaration(); + + // remove .exe extension - mainly to have the same output on UNIX and Windows + // NOLINTNEXTLINE(misc-const-correctness) + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if (binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + + xml.startElement("doctest").writeAttribute("binary", binary_name); + if (opt.no_version == false) + xml.writeAttribute("version", DOCTEST_VERSION_STR); + + // only the consequential ones (TODO: filters) + xml.scopedElement("Options") + .writeAttribute("order_by", opt.order_by.c_str()) + .writeAttribute("rand_seed", opt.rand_seed) + .writeAttribute("first", opt.first) + .writeAttribute("last", opt.last) + .writeAttribute("abort_after", opt.abort_after) + .writeAttribute("subcase_filter_levels", opt.subcase_filter_levels) + .writeAttribute("case_sensitive", opt.case_sensitive) + .writeAttribute("no_throw", opt.no_throw) + .writeAttribute("no_skip", opt.no_skip); +} + +void XmlReporter::test_run_end(const TestRunStats &p) { + if (tc) // the TestSuite tag - only if there has been at least 1 test case + xml.endElement(); + + xml.scopedElement("OverallResultsAsserts") + .writeAttribute("successes", p.numAsserts - p.numAssertsFailed) + .writeAttribute("failures", p.numAssertsFailed); + + xml.startElement("OverallResultsTestCases") + .writeAttribute("successes", p.numTestCasesPassingFilters - p.numTestCasesFailed) + .writeAttribute("failures", p.numTestCasesFailed); + if (opt.no_skipped_summary == false) + xml.writeAttribute("skipped", p.numTestCases - p.numTestCasesPassingFilters); + xml.endElement(); + + xml.endElement(); +} + +void XmlReporter::test_case_start(const TestCaseData &in) { + DOCTEST_LOCK_MUTEX(mutex) + test_case_start_impl(in); + xml.ensureTagClosed(); +} + +void XmlReporter::test_case_reenter(const TestCaseData &) {} + +void XmlReporter::test_case_end(const CurrentTestCaseStats &st) { + DOCTEST_LOCK_MUTEX(mutex) + xml.startElement("OverallResultsAsserts") + .writeAttribute("successes", st.numAssertsCurrentTest - st.numAssertsFailedCurrentTest) + .writeAttribute("failures", st.numAssertsFailedCurrentTest) + .writeAttribute("test_case_success", st.testCaseSuccess); + if (opt.duration) + xml.writeAttribute("duration", st.seconds); + if (tc->m_expected_failures) + xml.writeAttribute("expected_failures", tc->m_expected_failures); + xml.endElement(); + + xml.endElement(); +} + +void XmlReporter::test_case_exception(const TestCaseException &e) { + DOCTEST_LOCK_MUTEX(mutex) + + xml.scopedElement("Exception").writeAttribute("crash", e.is_crash).writeText(e.error_string.c_str()); +} + +void XmlReporter::subcase_start(const SubcaseSignature &in) { + DOCTEST_LOCK_MUTEX(mutex) + xml.startElement("SubCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file)) + .writeAttribute("line", line(in.m_line)); + xml.ensureTagClosed(); +} + +void XmlReporter::subcase_end() { + DOCTEST_LOCK_MUTEX(mutex) + xml.endElement(); +} + +void XmlReporter::log_assert(const AssertData &rb) { + if (!rb.m_failed && !opt.success) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + xml.startElement("Expression") + .writeAttribute("success", !rb.m_failed) + .writeAttribute("type", assertString(rb.m_at)) + .writeAttribute("filename", skipPathFromFilename(rb.m_file)) + .writeAttribute("line", line(rb.m_line)); + + xml.scopedElement("Original").writeText(rb.m_expr); + + if (rb.m_threw) + xml.scopedElement("Exception").writeText(rb.m_exception.c_str()); + + if (rb.m_at & assertType::is_throws_as) + xml.scopedElement("ExpectedException").writeText(rb.m_exception_type); + if (rb.m_at & assertType::is_throws_with) + xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string.c_str()); + if ((rb.m_at & assertType::is_normal) && !rb.m_threw) + xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str()); + + log_contexts(); + + xml.endElement(); +} + +void XmlReporter::log_message(const MessageData &mb) { + DOCTEST_LOCK_MUTEX(mutex) + + xml.startElement("Message") + .writeAttribute("type", failureString(mb.m_severity)) + .writeAttribute("filename", skipPathFromFilename(mb.m_file)) + .writeAttribute("line", line(mb.m_line)); + + xml.scopedElement("Text").writeText(mb.m_string.c_str()); + + log_contexts(); + + xml.endElement(); +} + +void XmlReporter::test_case_skipped(const TestCaseData &in) { + if (opt.no_skipped_summary == false) { + DOCTEST_LOCK_MUTEX(mutex) + test_case_start_impl(in); + xml.writeAttribute("skipped", "true"); + xml.endElement(); + } +} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) +#include +#endif + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +void FatalConditionHandler::reset() {} +void FatalConditionHandler::allocateAltStackMem() {} +void FatalConditionHandler::freeAltStackMem() {} +#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +void reportFatal(const std::string &message) { + g_cs->failure_flags |= TestCaseFailureReason::Crash; + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {message.c_str(), true}); + + for (size_t i = g_cs->traversal.unwindActiveSubcases(); i > 0; --i) + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + g_cs->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); +} + +#ifdef DOCTEST_PLATFORM_WINDOWS + +// There is no 1-1 mapping between signals and windows exceptions. +// Windows can easily distinguish between SO and SigSegV, +// but SigInt, SigTerm, etc are handled differently. +SignalDefs signalDefs[] = { + {static_cast(EXCEPTION_ILLEGAL_INSTRUCTION), "SIGILL - Illegal instruction signal"}, + {static_cast(EXCEPTION_STACK_OVERFLOW), "SIGSEGV - Stack overflow"}, + {static_cast(EXCEPTION_ACCESS_VIOLATION), "SIGSEGV - Segmentation violation signal"}, + {static_cast(EXCEPTION_INT_DIVIDE_BY_ZERO), "Divide by zero error"}, +}; + +LONG CALLBACK FatalConditionHandler::handleException(PEXCEPTION_POINTERS ExceptionInfo) { + // Multiple threads may enter this filter/handler at once. We want the error message to be + // printed on the console just once no matter how many threads have crashed. + DOCTEST_DECLARE_STATIC_MUTEX(mutex) + static bool execute = true; + { + DOCTEST_LOCK_MUTEX(mutex) + if (execute) { + bool reported = false; + for (size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + if (ExceptionInfo->ExceptionRecord->ExceptionCode == signalDefs[i].id) { + reportFatal(signalDefs[i].name); + reported = true; + break; + } + } + if (reported == false) + reportFatal("Unhandled SEH exception caught"); + if (isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + } + execute = false; + } + std::exit(EXIT_FAILURE); +} + +void FatalConditionHandler::allocateAltStackMem() {} +void FatalConditionHandler::freeAltStackMem() {} + +FatalConditionHandler::FatalConditionHandler() { + isSet = true; + // 32k seems enough for doctest to handle stack overflow, + // but the value was found experimentally, so there is no strong guarantee + guaranteeSize = 32 * 1024; + // Register an unhandled exception filter + previousTop = SetUnhandledExceptionFilter(handleException); + // Pass in guarantee size to be filled + SetThreadStackGuarantee(&guaranteeSize); + + // On Windows uncaught exceptions from another thread, exceptions from + // destructors, or calls to std::terminate are not a SEH exception + + // The terminal handler gets called when: + // - std::terminate is called FROM THE TEST RUNNER THREAD + // - an exception is thrown from a destructor FROM THE TEST RUNNER THREAD + original_terminate_handler = std::get_terminate(); + std::set_terminate([]() DOCTEST_NOEXCEPT { + reportFatal("Terminate handler called"); + if (isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); // explicitly exit - otherwise the SIGABRT handler may be called as well + }); + + // SIGABRT is raised when: + // - std::terminate is called FROM A DIFFERENT THREAD + // - an exception is thrown from a destructor FROM A DIFFERENT THREAD + // - an uncaught exception is thrown FROM A DIFFERENT THREAD + prev_sigabrt_handler = std::signal(SIGABRT, [](int signal) DOCTEST_NOEXCEPT { + if (signal == SIGABRT) { + reportFatal("SIGABRT - Abort (abnormal termination) signal"); + if (isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); + } + }); + + // The following settings are taken from google test, and more + // specifically from UnitTest::Run() inside of gtest.cc + + // the user does not want to see pop-up dialogs about crashes + prev_error_mode_1 = SetErrorMode( + SEM_FAILCRITICALERRORS | SEM_NOALIGNMENTFAULTEXCEPT | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX + ); + // This forces the abort message to go to stderr in all circumstances. + prev_error_mode_2 = _set_error_mode(_OUT_TO_STDERR); + // In the debug version, Visual Studio pops up a separate dialog + // offering a choice to debug the aborted program - we want to disable that. + prev_abort_behavior = _set_abort_behavior(0x0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + // In debug mode, the Windows CRT can crash with an assertion over invalid + // input (e.g. passing an invalid file descriptor). The default handling + // for these assertions is to pop up a dialog and wait for user input. + // Instead ask the CRT to dump such assertions to stderr non-interactively. + prev_report_mode = _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG); + prev_report_file = _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); +} + +void FatalConditionHandler::reset() { + if (isSet) { + // Unregister handler and restore the old guarantee + SetUnhandledExceptionFilter(previousTop); + SetThreadStackGuarantee(&guaranteeSize); + std::set_terminate(original_terminate_handler); + std::signal(SIGABRT, prev_sigabrt_handler); + SetErrorMode(prev_error_mode_1); + _set_error_mode(prev_error_mode_2); + _set_abort_behavior(prev_abort_behavior, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + static_cast(_CrtSetReportMode(_CRT_ASSERT, prev_report_mode)); + static_cast(_CrtSetReportFile(_CRT_ASSERT, prev_report_file)); + isSet = false; + } +} + +FatalConditionHandler::~FatalConditionHandler() { + reset(); +} + +UINT FatalConditionHandler::prev_error_mode_1; +int FatalConditionHandler::prev_error_mode_2; +unsigned int FatalConditionHandler::prev_abort_behavior; +int FatalConditionHandler::prev_report_mode; +_HFILE FatalConditionHandler::prev_report_file; +void(DOCTEST_CDECL *FatalConditionHandler::prev_sigabrt_handler)(int); +std::terminate_handler FatalConditionHandler::original_terminate_handler; +bool FatalConditionHandler::isSet = false; +ULONG FatalConditionHandler::guaranteeSize = 0; +LPTOP_LEVEL_EXCEPTION_FILTER FatalConditionHandler::previousTop = nullptr; + +#else // DOCTEST_PLATFORM_WINDOWS + +SignalDefs signalDefs[] = { + {SIGINT, "SIGINT - Terminal interrupt signal"}, + {SIGILL, "SIGILL - Illegal instruction signal"}, + {SIGFPE, "SIGFPE - Floating point error signal"}, + {SIGSEGV, "SIGSEGV - Segmentation violation signal"}, + {SIGTERM, "SIGTERM - Termination request signal"}, + {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"} +}; +static_assert( + DOCTEST_COUNTOF(signalDefs) == DOCTEST_COUNTOF(FatalConditionHandler::oldSigActions), "arrays should match in size" +); + +void FatalConditionHandler::handleSignal(int sig) { + const char *name = ""; + for (std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + const SignalDefs &def = signalDefs[i]; + if (sig == def.id) { + name = def.name; + break; + } + } + reset(); + reportFatal(name); + static_cast(raise(sig)); +} + +void FatalConditionHandler::allocateAltStackMem() { + altStackMem = new char[altStackSize]; +} + +void FatalConditionHandler::freeAltStackMem() { + delete[] altStackMem; +} + +FatalConditionHandler::FatalConditionHandler() { + isSet = true; + stack_t sigStack; + sigStack.ss_sp = altStackMem; + sigStack.ss_size = altStackSize; + sigStack.ss_flags = 0; + sigaltstack(&sigStack, &oldSigStack); + struct sigaction sa = {}; + sa.sa_handler = handleSignal; + sa.sa_flags = SA_ONSTACK; + for (std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &sa, &oldSigActions[i]); + } +} + +FatalConditionHandler::~FatalConditionHandler() { + reset(); +} + +void FatalConditionHandler::reset() { + if (isSet) { + // Set signals back to previous values -- hopefully nobody overwrote them in the meantime + for (std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &oldSigActions[i], nullptr); + } + // Return the old stack + sigaltstack(&oldSigStack, nullptr); + isSet = false; + } +} + +bool FatalConditionHandler::isSet = false; +struct sigaction FatalConditionHandler::oldSigActions[DOCTEST_COUNTOF(signalDefs)] = {}; +stack_t FatalConditionHandler::oldSigStack = {}; +size_t FatalConditionHandler::altStackSize = 4 * SIGSTKSZ; +char *FatalConditionHandler::altStackMem = nullptr; + +#endif // DOCTEST_PLATFORM_WINDOWS +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +DOCTEST_THREAD_LOCAL class oss { + std::vector stack; + std::stringstream ss; + +public: + std::ostream *push() { + stack.push_back(ss.tellp()); + return &ss; + } + + String pop() { + if (stack.empty()) + DOCTEST_INTERNAL_ERROR("TLSS was empty when trying to pop!"); + + if (ss.fail()) + DOCTEST_INTERNAL_ERROR("Output stream is bad"); + + const std::streampos pos = stack.back(); + stack.pop_back(); + const unsigned sz = static_cast(ss.tellp() - pos); + ss.rdbuf()->pubseekpos(pos, std::ios::in | std::ios::out); + return String(ss, sz); + } +} g_oss; // NOLINT(bugprone-throwing-static-initialization, cert-err58-cpp) + +std::ostream *tlssPush() { + return g_oss.push(); +} + +String tlssPop() { + return g_oss.pop(); +} + +} // namespace detail + +// case insensitive strcmp +static int stricmp(const char *a, const char *b) { + for (;; a++, b++) { + const int d = tolower(*a) - tolower(*b); + if (d != 0 || !*a) + return d; + } +} + +// NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) + +char *String::allocate(size_type sz) { + if (sz <= last) { + buf[sz] = '\0'; + setLast(last - sz); + return buf; + } else { + setOnHeap(); + data.size = sz; + data.capacity = data.size + 1; + data.ptr = new char[data.capacity]; + data.ptr[sz] = '\0'; + return data.ptr; + } +} + +void String::setOnHeap() noexcept { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + *reinterpret_cast(&buf[last]) = 128; +} + +void String::setLast(size_type in) noexcept { + buf[last] = static_cast(in); +} + +void String::setSize(size_type sz) noexcept { + if (isOnStack()) { + buf[sz] = '\0'; + setLast(last - sz); + } else { + data.ptr[sz] = '\0'; + data.size = sz; + } +} + +void String::copy(const String &other) { + if (other.isOnStack()) { + memcpy(buf, other.buf, len); + } else { + memcpy(allocate(other.data.size), other.data.ptr, other.data.size); + } +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +String::String() noexcept { + buf[0] = '\0'; + setLast(); +} + +String::~String() { + if (!isOnStack()) + delete[] data.ptr; +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +String::String(const char *in) + : String(in, strlen(in)) {} + +String::String(const char *in, size_type in_size) { + memcpy(allocate(in_size), in, in_size); +} + +String::String(std::istream &in, size_type in_size) { + in.read(allocate(in_size), in_size); +} + +String::String(const String &other) { + copy(other); +} + +String &String::operator=(const String &other) { + if (this != &other) { + if (!isOnStack()) + delete[] data.ptr; + + copy(other); + } + + return *this; +} + +String &String::operator+=(const String &other) { + const size_type my_old_size = size(); + const size_type other_size = other.size(); + const size_type total_size = my_old_size + other_size; + if (isOnStack()) { + if (total_size < len) { + // append to the current stack space + memcpy(buf + my_old_size, other.c_str(), other_size + 1); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + setLast(last - total_size); + } else { + // alloc new chunk + char *temp = new char[total_size + 1]; + // copy current data to new location before writing in the union + memcpy(temp, buf, my_old_size); // skip the +1 ('\0') for speed + // update data in union + setOnHeap(); + data.size = total_size; + data.capacity = data.size + 1; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } else { + if (data.capacity > total_size) { + // append to the current heap block + data.size = total_size; + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } else { + // resize + data.capacity *= 2; + if (data.capacity <= total_size) + data.capacity = total_size + 1; + // alloc new chunk + char *temp = new char[data.capacity]; + // copy current data to new location before releasing it + memcpy(temp, data.ptr, my_old_size); // skip the +1 ('\0') for speed + // release old chunk + delete[] data.ptr; + // update the rest of the union members + data.size = total_size; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } + + return *this; +} + +String::String(String &&other) noexcept { + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); +} + +String &String::operator=(String &&other) noexcept { + if (this != &other) { + if (!isOnStack()) + delete[] data.ptr; + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); + } + return *this; +} + +char String::operator[](size_type i) const { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + return const_cast(this)->operator[](i); +} + +char &String::operator[](size_type i) { + if (isOnStack()) + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(buf)[i]; + return data.ptr[i]; +} + +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmaybe-uninitialized") +String::size_type String::size() const { + if (isOnStack()) + return last - (static_cast(buf[last]) & 31); // using "last" would work only if "len" is 32 + return data.size; +} +DOCTEST_GCC_SUPPRESS_WARNING_POP + +String::size_type String::capacity() const { + if (isOnStack()) + return len; + return data.capacity; +} + +String String::substr(size_type pos, size_type cnt) && { + cnt = std::min(cnt, size() - pos); + char *cptr = c_str(); + memmove(cptr, cptr + pos, cnt); + setSize(cnt); + return std::move(*this); +} + +String String::substr(size_type pos, size_type cnt) const & { + cnt = std::min(cnt, size() - pos); + return String{c_str() + pos, cnt}; +} + +String::size_type String::find(char ch, size_type pos) const { + const char *begin = c_str(); + const char *end = begin + size(); + const char *it = begin + pos; + for (; it < end && *it != ch; it++) {} + if (it < end) { + return static_cast(it - begin); + } else { + return npos; + } +} + +String::size_type String::rfind(char ch, size_type pos) const { + if (size() == 0) { + return npos; + } + + const char *begin = c_str(); + const char *it = begin + std::min(pos, size() - 1); + for (; it >= begin && *it != ch; it--) {} + if (it >= begin) { + return static_cast(it - begin); + } else { + return npos; + } +} + +int String::compare(const char *other, bool no_case) const { + if (no_case) + return doctest::stricmp(c_str(), other); + return std::strcmp(c_str(), other); +} + +int String::compare(const String &other, bool no_case) const { + return compare(other.c_str(), no_case); +} + +String operator+(const String &lhs, const String &rhs) { + return String(lhs) += rhs; +} + +bool operator==(const String &lhs, const String &rhs) { + return lhs.compare(rhs) == 0; +} + +bool operator!=(const String &lhs, const String &rhs) { + return lhs.compare(rhs) != 0; +} + +bool operator<(const String &lhs, const String &rhs) { + return lhs.compare(rhs) < 0; +} + +bool operator>(const String &lhs, const String &rhs) { + return lhs.compare(rhs) > 0; +} + +bool operator<=(const String &lhs, const String &rhs) { + return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; +} + +bool operator>=(const String &lhs, const String &rhs) { + return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; +} + +std::ostream &operator<<(std::ostream &s, const String &in) { + return s << in.c_str(); +} + +namespace detail { + +void filldata::fill(std::ostream *stream, const void *in) { + filldata::fill(stream, in); +} + +void filldata::fill(std::ostream *stream, const volatile void *in) { + if (in) { + *stream << in; + } else { + *stream << "nullptr"; + } +} + +template +String toStreamLit(T t) { + // NOLINTNEXTLINE(misc-const-correctness) + std::ostream *os = tlssPush(); + os->operator<<(t); + return tlssPop(); +} +} // namespace detail + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(const char *in) { + return String("\"") + (in ? in : "{null string}") + "\""; +} +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +String toString(const std::string &in) { + return in.c_str(); +} +#endif // VS 2019 + +String toString(const String &in) { + return in; +} + +String toString(std::nullptr_t) { + return "nullptr"; +} + +String toString(bool in) { + return in ? "true" : "false"; +} + +String toString(float in) { + return detail::toStreamLit(in); +} +String toString(double in) { + return detail::toStreamLit(in); +} +String toString(double long in) { + return detail::toStreamLit(in); +} + +String toString(char in) { + return detail::toStreamLit(static_cast(in)); +} +String toString(char signed in) { + return detail::toStreamLit(static_cast(in)); +} +String toString(char unsigned in) { + return detail::toStreamLit(static_cast(in)); +} +String toString(short in) { + return detail::toStreamLit(in); +} +String toString(short unsigned in) { + return detail::toStreamLit(in); +} +String toString(signed in) { + return detail::toStreamLit(in); +} +String toString(unsigned in) { + return detail::toStreamLit(in); +} +String toString(long in) { + return detail::toStreamLit(in); +} +String toString(long unsigned in) { + return detail::toStreamLit(in); +} +String toString(long long in) { + return detail::toStreamLit(in); +} +String toString(long long unsigned in) { + return detail::toStreamLit(in); +} + +// NOLINTEND(cppcoreguidelines-pro-type-union-access) + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +bool SubcaseSignature::operator==(const SubcaseSignature &other) const { + return m_line == other.m_line && std::strcmp(m_file, other.m_file) == 0 && m_name == other.m_name; +} + +bool SubcaseSignature::operator<(const SubcaseSignature &other) const { + if (m_line != other.m_line) + return m_line < other.m_line; + if (std::strcmp(m_file, other.m_file) != 0) + return std::strcmp(m_file, other.m_file) < 0; + return m_name.compare(other.m_name) < 0; +} + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { + +bool Subcase::checkFilters() { + if (g_cs->traversal.activeSubcaseDepth() < static_cast(g_cs->subcase_filter_levels)) { + if (!matchesAny(m_signature.m_name.c_str(), g_cs->filters[6], true, g_cs->case_sensitive)) + return true; + if (matchesAny(m_signature.m_name.c_str(), g_cs->filters[7], false, g_cs->case_sensitive)) + return true; + } + return false; +} + +Subcase::Subcase(const String &name, const char *file, int line) + : m_signature({name, file, line}) { + if (checkFilters()) + return; + + if (!g_cs->traversal.tryEnterSubcase(m_signature)) + return; + + m_entered = true; + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); +} + +Subcase::~Subcase() { + if (m_entered) { + g_cs->traversal.leaveSubcase(); + + if (detail::has_uncaught_exceptions() && g_cs->shouldLogCurrentException) { + DOCTEST_ITERATE_THROUGH_REPORTERS( + test_case_exception, + {"exception thrown in subcase - will translate later " + "when the whole test case has been exited (cannot " + "translate while there is an active exception)", + false} + ); + g_cs->shouldLogCurrentException = false; + } + + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } +} + +Subcase::operator bool() const { + return m_entered; +} + +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +std::set &getRegisteredTests() { + static std::set data; + return data; +} + +TestCase::TestCase( + funcType test, const char *file, unsigned line, const TestSuite &test_suite, const String &type, int template_id +) noexcept { + m_file = file; + m_line = line; + m_name = nullptr; // will be later overridden in operator* + m_test_suite = test_suite.m_test_suite; + m_description = test_suite.m_description; + m_skip = test_suite.m_skip; + m_no_breaks = test_suite.m_no_breaks; + m_no_output = test_suite.m_no_output; + m_may_fail = test_suite.m_may_fail; + m_should_fail = test_suite.m_should_fail; + m_expected_failures = test_suite.m_expected_failures; + m_timeout = test_suite.m_timeout; + + m_test = test; + m_type = type; + m_template_id = template_id; +} + +TestCase::TestCase(const TestCase &other) noexcept // NOLINT(bugprone-copy-constructor-init) + : TestCaseData() { + *this = other; +} + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function +TestCase &TestCase::operator=(const TestCase &other) noexcept { // NOLINT(cert-oop54-cpp) + TestCaseData::operator=(other); + m_test = other.m_test; + m_type = other.m_type; + m_template_id = other.m_template_id; + m_full_name = other.m_full_name; + + if (m_template_id != -1) + m_name = m_full_name.c_str(); + return *this; +} +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +TestCase &TestCase::operator*(const char *in) noexcept { + m_name = in; + // make a new name with an appended type for templated test case + if (m_template_id != -1) { + m_full_name = String(m_name) + "<" + m_type + ">"; + // redirect the name to point to the newly constructed full name + m_name = m_full_name.c_str(); + } + return *this; +} + +bool TestCase::operator<(const TestCase &other) const noexcept { + // this will be used only to differentiate between test cases - not relevant for sorting + if (m_line != other.m_line) + return m_line < other.m_line; + const int name_cmp = strcmp(m_name, other.m_name); + if (name_cmp != 0) + return name_cmp < 0; + const int file_cmp = m_file.compare(other.m_file); + if (file_cmp != 0) + return file_cmp < 0; + return m_template_id < other.m_template_id; +} + +// used by the macros for registering tests +int regTest(const TestCase &tc) noexcept { + getRegisteredTests().insert(tc); + return 0; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +TestSuite &TestSuite::operator*(const char *in) noexcept { + m_test_suite = in; + return *this; +} + +// sets the current test suite +int setTestSuite(const TestSuite &ts) noexcept { + doctest_detail_test_suite_ns::getCurrentTestSuite() = ts; + return 0; +} + +} // namespace detail +} // namespace doctest + +namespace doctest_detail_test_suite_ns { +// holds the current test suite +doctest::detail::TestSuite &getCurrentTestSuite() noexcept { + static doctest::detail::TestSuite data{}; + return data; +} +} // namespace doctest_detail_test_suite_ns + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_CONFIG_GETCURRENTTICKS +uint64_t getCurrentTicks() { + return DOCTEST_CONFIG_GETCURRENTTICKS(); +} +#elif defined(DOCTEST_PLATFORM_WINDOWS) +uint64_t getCurrentTicks() { + static LARGE_INTEGER hz = {{0}}, hzo = {{0}}; + if (!hz.QuadPart) { + QueryPerformanceFrequency(&hz); + QueryPerformanceCounter(&hzo); + } + LARGE_INTEGER t; + QueryPerformanceCounter(&t); + return ((t.QuadPart - hzo.QuadPart) * LONGLONG(1000000)) / hz.QuadPart; +} +#else // DOCTEST_PLATFORM_WINDOWS +uint64_t getCurrentTicks() { + timeval t; + gettimeofday(&t, nullptr); + return static_cast(t.tv_sec) * 1000000 + static_cast(t.tv_usec); +} +#endif // DOCTEST_PLATFORM_WINDOWS + +void Timer::start() { + m_ticks = getCurrentTicks(); +} + +unsigned int Timer::getElapsedMicroseconds() const { + return static_cast(getCurrentTicks() - m_ticks); +} + +// unsigned int Timer::getElapsedMilliseconds() const { +// return static_cast(getElapsedMicroseconds() / 1000); +// } + +double Timer::getElapsedSeconds() const { + return static_cast(getCurrentTicks() - m_ticks) / 1000000.0; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + + +#include + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +DOCTEST_NOINLINE DecisionPoint &TraversalState::ensureDecisionPointAtCurrentDepth() { + const size_t depth = m_decisionDepth; + + if (m_discoveredDecisionPath.size() == depth) { + m_discoveredDecisionPath.emplace_back(); + + if (m_decisionPath.size() == depth) + m_decisionPath.push_back(0); + } + + return m_discoveredDecisionPath[depth]; +} + +void TraversalState::resetForTestCase() { + m_decisionPath.clear(); + m_discoveredDecisionPath.clear(); + m_enteredSubcaseDepths.clear(); + m_activeSubcaseDepth = 0; + m_decisionDepth = 0; +} + +void TraversalState::resetForRun() { + m_activeSubcaseDepth = 0; + m_discoveredDecisionPath.clear(); + m_decisionDepth = 0; + m_enteredSubcaseDepths.clear(); +} + +bool TraversalState::advance() { + const size_t maxDepth = std::min(m_decisionPath.size(), m_discoveredDecisionPath.size()); + for (size_t depth = maxDepth; depth > 0; --depth) { + const size_t index = depth - 1; + if (m_decisionPath[index] + 1 < m_discoveredDecisionPath[index].branch_count) { + ++m_decisionPath[index]; + m_decisionPath.resize(index + 1); + return true; + } + } + + return false; +} + +bool TraversalState::tryEnterSubcase(const SubcaseSignature &signature) { + DecisionPoint &point = ensureDecisionPointAtCurrentDepth(); + std::vector &subcases = point.subcases; + size_t siblingIndex = 0; + + for (; siblingIndex < subcases.size(); ++siblingIndex) { + if (subcases[siblingIndex] == signature) + break; + } + + if (siblingIndex == subcases.size()) + subcases.push_back(signature); + + point.branch_count = subcases.size(); + + if (siblingIndex != m_decisionPath[m_decisionDepth]) + return false; + + m_enteredSubcaseDepths.push_back(m_decisionDepth); + m_activeSubcaseDepth++; + m_decisionDepth++; + return true; +} + +void TraversalState::leaveSubcase() { + m_decisionDepth = m_enteredSubcaseDepths.back(); + m_enteredSubcaseDepths.pop_back(); + m_activeSubcaseDepth--; +} + +size_t TraversalState::unwindActiveSubcases() { + const size_t activeSubcaseCount = m_activeSubcaseDepth; + + while (m_activeSubcaseDepth > 0) + leaveSubcase(); + + return activeSubcaseCount; +} + +size_t TraversalState::acquireGeneratorIndex(size_t count) { + DecisionPoint &point = ensureDecisionPointAtCurrentDepth(); + point.branch_count = count; + + const size_t index = m_decisionPath[m_decisionDepth]; + m_decisionDepth++; + return index < count ? index : 0; +} + +size_t acquireGeneratorDecisionIndex(size_t count) { + return g_cs->traversal.acquireGeneratorIndex(count); +} +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= +/* clang-format off */ /* NOLINTBEGIN */ + +using uchar = unsigned char; + + size_t trailingBytes(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return 2; + } + if ((c & 0xF0) == 0xE0) { + return 3; + } + if ((c & 0xF8) == 0xF0) { + return 4; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + uint32_t headerValue(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return c & 0x1F; + } + if ((c & 0xF0) == 0xE0) { + return c & 0x0F; + } + if ((c & 0xF8) == 0xF0) { + return c & 0x07; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + void hexEscapeChar(std::ostream& os, unsigned char c) { + std::ios_base::fmtflags f(os.flags()); + os << "\\x" + << std::uppercase << std::hex << std::setfill('0') << std::setw(2) + << static_cast(c); + os.flags(f); + } + + XmlEncode::XmlEncode( std::string const& str, ForWhat forWhat ) + : m_str( str ), + m_forWhat( forWhat ) + {} + + void XmlEncode::encodeTo( std::ostream& os ) const { + // Apostrophe escaping not necessary if we always use " to write attributes + // (see: https://www.w3.org/TR/xml/#syntax) + + for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) { + uchar c = m_str[idx]; + switch (c) { + case '<': os << "<"; break; + case '&': os << "&"; break; + + case '>': + // See: https://www.w3.org/TR/xml/#syntax + if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']') + os << ">"; + else + os << c; + break; + + case '\"': + if (m_forWhat == ForAttributes) + os << """; + else + os << c; + break; + + default: + // Check for control characters and invalid utf-8 + + // Escape control characters in standard ascii + // see https://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 + if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) { + hexEscapeChar(os, c); + break; + } + + // Plain ASCII: Write it to stream + if (c < 0x7F) { + os << c; + break; + } + + // UTF-8 territory + // Check if the encoding is valid and if it is not, hex escape bytes. + // Important: We do not check the exact decoded values for validity, only the encoding format + // First check that this bytes is a valid lead byte: + // This means that it is not encoded as 1111 1XXX + // Or as 10XX XXXX + if (c < 0xC0 || + c >= 0xF8) { + hexEscapeChar(os, c); + break; + } + + auto encBytes = trailingBytes(c); + // Are there enough bytes left to avoid accessing out-of-bounds memory? + if (idx + encBytes - 1 >= m_str.size()) { + hexEscapeChar(os, c); + break; + } + // The header is valid, check data + // The next encBytes bytes must together be a valid utf-8 + // This means: bitpattern 10XX XXXX and the extracted value is sane (ish) + bool valid = true; + uint32_t value = headerValue(c); + for (std::size_t n = 1; n < encBytes; ++n) { + uchar nc = m_str[idx + n]; + valid &= ((nc & 0xC0) == 0x80); + value = (value << 6) | (nc & 0x3F); + } + + if ( + // Wrong bit pattern of following bytes + (!valid) || + // Overlong encodings + (value < 0x80) || + ( value < 0x800 && encBytes > 2) || // removed "0x80 <= value &&" because redundant + (0x800 < value && value < 0x10000 && encBytes > 3) || + // Encoded value out of range + (value >= 0x110000) + ) { + hexEscapeChar(os, c); + break; + } + + // If we got here, this is in fact a valid(ish) utf-8 sequence + for (std::size_t n = 0; n < encBytes; ++n) { + os << m_str[idx + n]; + } + idx += encBytes - 1; + break; + } + } + } + + std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) { + xmlEncode.encodeTo( os ); + return os; + } + + XmlWriter::ScopedElement::ScopedElement( XmlWriter* writer ) + : m_writer( writer ) + {} + + XmlWriter::ScopedElement::ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT + : m_writer( other.m_writer ){ + other.m_writer = nullptr; + } + XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT { + if ( m_writer ) { + m_writer->endElement(); + } + m_writer = other.m_writer; + other.m_writer = nullptr; + return *this; + } + + + XmlWriter::ScopedElement::~ScopedElement() { + if( m_writer ) + m_writer->endElement(); + } + + XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText( std::string const& text, bool indent ) { + m_writer->writeText( text, indent ); + return *this; + } + + XmlWriter::XmlWriter( std::ostream& os ) : m_os( os ) + { + // writeDeclaration(); // called explicitly by the reporters that use the writer class - see issue #627 + } + + XmlWriter::~XmlWriter() { + while( !m_tags.empty() ) + endElement(); + } + + XmlWriter& XmlWriter::startElement( std::string const& name ) { + ensureTagClosed(); + newlineIfNecessary(); + m_os << m_indent << '<' << name; + m_tags.push_back( name ); + m_indent += " "; + m_tagIsOpen = true; + return *this; + } + + XmlWriter::ScopedElement XmlWriter::scopedElement( std::string const& name ) { + ScopedElement scoped( this ); + startElement( name ); + return scoped; + } + + XmlWriter& XmlWriter::endElement() { + newlineIfNecessary(); + m_indent = m_indent.substr( 0, m_indent.size()-2 ); + if( m_tagIsOpen ) { + m_os << "/>"; + m_tagIsOpen = false; + } + else { + m_os << m_indent << ""; + } + m_os << std::endl; + m_tags.pop_back(); + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, std::string const& attribute ) { + if( !name.empty() && !attribute.empty() ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, const char* attribute ) { + if( !name.empty() && attribute && attribute[0] != '\0' ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, bool attribute ) { + m_os << ' ' << name << "=\"" << ( attribute ? "true" : "false" ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeText( std::string const& text, bool indent ) { + if( !text.empty() ){ + bool tagWasOpen = m_tagIsOpen; + ensureTagClosed(); + if( tagWasOpen && indent ) + m_os << m_indent; + m_os << XmlEncode( text ); + m_needsNewline = true; + } + return *this; + } + + //XmlWriter& XmlWriter::writeComment( std::string const& text ) { + // ensureTagClosed(); + // m_os << m_indent << ""; + // m_needsNewline = true; + // return *this; + //} + + //void XmlWriter::writeStylesheetRef( std::string const& url ) { + // m_os << "\n"; + //} + + //XmlWriter& XmlWriter::writeBlankLine() { + // ensureTagClosed(); + // m_os << '\n'; + // return *this; + //} + + void XmlWriter::ensureTagClosed() { + if( m_tagIsOpen ) { + m_os << ">" << std::endl; + m_tagIsOpen = false; + } + } + + void XmlWriter::writeDeclaration() { + m_os << "\n"; + } + + void XmlWriter::newlineIfNecessary() { + if( m_needsNewline ) { + m_os << std::endl; + m_needsNewline = false; + } + } + +/* clang-format on */ /* NOLINTEND */ +// ================================================================================================= +// End of copy-pasted code from Catch +// ================================================================================================= + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // defined(DOCTEST_CONFIG_IMPLEMENT) && !defined(DOCTEST_LIBRARY_IMPLEMENTATION) From 4087aaf53df9933535fb21eb53ef78e01f0ff213 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 17:14:08 +0200 Subject: [PATCH 03/63] ci: run the native workflows on pull requests to dev Both workflows only triggered on master, so nothing integrating into dev was tested. main.yml already carried a commented-out dev push trigger. --- .github/workflows/macos.yml | 2 +- .github/workflows/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 0a078f7a..7df9b1ae 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -5,7 +5,7 @@ on: branches: - brew pull_request: - branches: [ "master" ] + branches: [ "master", "dev" ] workflow_dispatch: jobs: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 86e6dcba..32d68f28 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,9 +4,9 @@ on: push: branches: - "master" - # - "dev" + - "dev" pull_request: - branches: [ "master" ] + branches: [ "master", "dev" ] workflow_dispatch: jobs: From 27e07410ef1afc6adb0008f978512278d048fc87 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 17:18:08 +0200 Subject: [PATCH 04/63] ci: take ownership of build/ before running ctest on Linux The Linux workflow builds with `sudo bash ./install-forefire.sh -y`, so build/ is root-owned and ctest cannot create build/Testing/Temporary as the runner user. It exited 8 without running a single test. macOS is unaffected: install-forefire-osx.sh runs unprivileged. --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 32d68f28..f33b045c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,7 +49,11 @@ jobs: # call at a time, which is what runff below cannot reach: it exercises a # single case through Rothermel. - name: Run C++ unit tests - run: ctest --test-dir build --output-on-failure + run: | + # The build step above runs under sudo, so build/ is root-owned and + # ctest cannot create build/Testing/Temporary as the runner user. + sudo chown -R "$(id -u):$(id -g)" build + ctest --test-dir build --output-on-failure - name: Add Build/Runtime Diagnostics run: | From 0196cd07eecc63c5a07bdc7c2e40b4aabdbb13c2 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 17:27:07 +0200 Subject: [PATCH 05/63] fix(models): stop freeing the properties array twice `properties` is allocated by each model's own constructor with `new double[numProperties]` and freed by ~ForeFireModel. Sixteen flux models and two propagation models freed it again in their own destructor, so destroying any model that registers at least one property was a double free. The flux ones also used scalar `delete` on an array allocated with `new[]`. Removing the derived deletes and leaving it to the base class is the whole fix; no ownership changes. This has never been hit in production because FireDomain keeps its models in propModelsTable and fluxModelsTable and frees neither, so nothing destroys a model today. It is reachable from anything that does, including a test -- which is why the test that covers it comes with it: `every model can be destroyed` now runs all 33 models rather than the six that register no properties, and `destroying a model does not disturb the next one` allocates across a destruction, since a double free often surfaces as the next allocation coming back wrong rather than as an immediate abort. Verified by reintroducing a single delete: the suite aborts with "double free or corruption (!prev)" and doctest reports the case as CRASHED - SIGABRT. --- src/flux/BurnupHeatFluxModel.cpp | 1 - src/flux/CraterHeatFluxModel.cpp | 1 - src/flux/CraterVaporFluxModel.cpp | 1 - src/flux/FactorChemFluxModel.cpp | 1 - src/flux/ForeFireV1HeatFluxModel.cpp | 1 - src/flux/ForeFireV1VaporFluxModel.cpp | 1 - src/flux/HeatFluxBasicModel.cpp | 1 - src/flux/HeatFluxFromObsModel.cpp | 1 - src/flux/HeatFluxNominalModel.cpp | 1 - src/flux/LavaSO2FluxModel.cpp | 1 - src/flux/ScalarFluxNominalModel.cpp | 1 - src/flux/ScalarFromObsModel.cpp | 1 - src/flux/SpottingFluxBasicModel.cpp | 1 - src/flux/VaporFluxBasicModel.cpp | 1 - src/flux/VaporFluxFromObsModel.cpp | 1 - src/flux/VaporFluxNominalModel.cpp | 1 - src/propagation/ANNPropagationModel.cpp | 3 - src/propagation/BMapLoggerForANNTraining.cpp | 3 - tests/unit/README.md | 37 ++++++------ tests/unit/test_model_registry.cpp | 62 +++++++++++++------- 20 files changed, 59 insertions(+), 62 deletions(-) diff --git a/src/flux/BurnupHeatFluxModel.cpp b/src/flux/BurnupHeatFluxModel.cpp index ba8654a6..3cab31c4 100644 --- a/src/flux/BurnupHeatFluxModel.cpp +++ b/src/flux/BurnupHeatFluxModel.cpp @@ -88,7 +88,6 @@ BurnupHeatFluxModel::BurnupHeatFluxModel( /* destructor (shoudn't be modified) */ BurnupHeatFluxModel::~BurnupHeatFluxModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/CraterHeatFluxModel.cpp b/src/flux/CraterHeatFluxModel.cpp index 6e8b75ed..38b0c89d 100644 --- a/src/flux/CraterHeatFluxModel.cpp +++ b/src/flux/CraterHeatFluxModel.cpp @@ -112,7 +112,6 @@ CraterHeatFluxModel::CraterHeatFluxModel( /* destructor (shoudn't be modified) */ CraterHeatFluxModel::~CraterHeatFluxModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/CraterVaporFluxModel.cpp b/src/flux/CraterVaporFluxModel.cpp index 519065ef..eab3d529 100644 --- a/src/flux/CraterVaporFluxModel.cpp +++ b/src/flux/CraterVaporFluxModel.cpp @@ -93,7 +93,6 @@ CraterVaporFluxModel::CraterVaporFluxModel( /* destructor (shoudn't be modified) */ CraterVaporFluxModel::~CraterVaporFluxModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/FactorChemFluxModel.cpp b/src/flux/FactorChemFluxModel.cpp index 2e70442c..97663e49 100644 --- a/src/flux/FactorChemFluxModel.cpp +++ b/src/flux/FactorChemFluxModel.cpp @@ -100,7 +100,6 @@ FactorChemFluxModel::FactorChemFluxModel( /* destructor (shoudn't be modified) */ FactorChemFluxModel::~FactorChemFluxModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/ForeFireV1HeatFluxModel.cpp b/src/flux/ForeFireV1HeatFluxModel.cpp index 859bc473..1c4e7274 100644 --- a/src/flux/ForeFireV1HeatFluxModel.cpp +++ b/src/flux/ForeFireV1HeatFluxModel.cpp @@ -116,7 +116,6 @@ ForeFireV1HeatFluxModel::ForeFireV1HeatFluxModel( /* destructor (shoudn't be modified) */ ForeFireV1HeatFluxModel::~ForeFireV1HeatFluxModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/ForeFireV1VaporFluxModel.cpp b/src/flux/ForeFireV1VaporFluxModel.cpp index 413a7ccc..11a9c657 100644 --- a/src/flux/ForeFireV1VaporFluxModel.cpp +++ b/src/flux/ForeFireV1VaporFluxModel.cpp @@ -112,7 +112,6 @@ ForeFireV1VaporFluxModel::ForeFireV1VaporFluxModel( /* destructor (shoudn't be modified) */ ForeFireV1VaporFluxModel::~ForeFireV1VaporFluxModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/HeatFluxBasicModel.cpp b/src/flux/HeatFluxBasicModel.cpp index 37ed1799..5cdcf766 100644 --- a/src/flux/HeatFluxBasicModel.cpp +++ b/src/flux/HeatFluxBasicModel.cpp @@ -78,7 +78,6 @@ HeatFluxBasicModel::HeatFluxBasicModel( /* destructor (shoudn't be modified) */ HeatFluxBasicModel::~HeatFluxBasicModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/HeatFluxFromObsModel.cpp b/src/flux/HeatFluxFromObsModel.cpp index 8772f740..3b6f84fe 100644 --- a/src/flux/HeatFluxFromObsModel.cpp +++ b/src/flux/HeatFluxFromObsModel.cpp @@ -152,7 +152,6 @@ HeatFluxFromObsModel::HeatFluxFromObsModel( /* destructor (shoudn't be modified) */ HeatFluxFromObsModel::~HeatFluxFromObsModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/HeatFluxNominalModel.cpp b/src/flux/HeatFluxNominalModel.cpp index bd0dc8a8..dda6b3c0 100644 --- a/src/flux/HeatFluxNominalModel.cpp +++ b/src/flux/HeatFluxNominalModel.cpp @@ -75,7 +75,6 @@ HeatFluxNominalModel::HeatFluxNominalModel( /* destructor (shoudn't be modified) */ HeatFluxNominalModel::~HeatFluxNominalModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/LavaSO2FluxModel.cpp b/src/flux/LavaSO2FluxModel.cpp index f595771d..26bc324f 100644 --- a/src/flux/LavaSO2FluxModel.cpp +++ b/src/flux/LavaSO2FluxModel.cpp @@ -98,7 +98,6 @@ LavaSO2FluxModel::LavaSO2FluxModel( /* destructor (shoudn't be modified) */ LavaSO2FluxModel::~LavaSO2FluxModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/ScalarFluxNominalModel.cpp b/src/flux/ScalarFluxNominalModel.cpp index 267d053f..1cb9dbe6 100644 --- a/src/flux/ScalarFluxNominalModel.cpp +++ b/src/flux/ScalarFluxNominalModel.cpp @@ -75,7 +75,6 @@ ScalarFluxNominalModel::ScalarFluxNominalModel( /* destructor (shoudn't be modified) */ ScalarFluxNominalModel::~ScalarFluxNominalModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/ScalarFromObsModel.cpp b/src/flux/ScalarFromObsModel.cpp index 083ee9af..ad8f1c72 100644 --- a/src/flux/ScalarFromObsModel.cpp +++ b/src/flux/ScalarFromObsModel.cpp @@ -99,7 +99,6 @@ ScalarFromObsModel::ScalarFromObsModel( /* destructor (shoudn't be modified) */ ScalarFromObsModel::~ScalarFromObsModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/SpottingFluxBasicModel.cpp b/src/flux/SpottingFluxBasicModel.cpp index f29be52c..36d40414 100644 --- a/src/flux/SpottingFluxBasicModel.cpp +++ b/src/flux/SpottingFluxBasicModel.cpp @@ -100,7 +100,6 @@ SpottingFluxBasicModel::SpottingFluxBasicModel( /* destructor (shoudn't be modified) */ SpottingFluxBasicModel::~SpottingFluxBasicModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/VaporFluxBasicModel.cpp b/src/flux/VaporFluxBasicModel.cpp index ed80c8d6..dbe24245 100644 --- a/src/flux/VaporFluxBasicModel.cpp +++ b/src/flux/VaporFluxBasicModel.cpp @@ -77,7 +77,6 @@ VaporFluxBasicModel::VaporFluxBasicModel( /* destructor (shoudn't be modified) */ VaporFluxBasicModel::~VaporFluxBasicModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/VaporFluxFromObsModel.cpp b/src/flux/VaporFluxFromObsModel.cpp index f8009f1d..f9f85e2a 100644 --- a/src/flux/VaporFluxFromObsModel.cpp +++ b/src/flux/VaporFluxFromObsModel.cpp @@ -75,7 +75,6 @@ VaporFluxFromObsModel::VaporFluxFromObsModel( /* destructor (shoudn't be modified) */ VaporFluxFromObsModel::~VaporFluxFromObsModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/flux/VaporFluxNominalModel.cpp b/src/flux/VaporFluxNominalModel.cpp index 327d5880..95494895 100644 --- a/src/flux/VaporFluxNominalModel.cpp +++ b/src/flux/VaporFluxNominalModel.cpp @@ -74,7 +74,6 @@ VaporFluxNominalModel::VaporFluxNominalModel( /* destructor (shoudn't be modified) */ VaporFluxNominalModel::~VaporFluxNominalModel() { - if ( properties != 0 ) delete properties; } /* accessor to the name of the model */ diff --git a/src/propagation/ANNPropagationModel.cpp b/src/propagation/ANNPropagationModel.cpp index ff0cdd10..9429521c 100644 --- a/src/propagation/ANNPropagationModel.cpp +++ b/src/propagation/ANNPropagationModel.cpp @@ -73,9 +73,6 @@ ANNPropagationModel::ANNPropagationModel(const int & mindex, DataBroker* db) ANNPropagationModel::~ANNPropagationModel() { - if (properties) { - delete[] properties; - } } /* accessor to the name of the model */ diff --git a/src/propagation/BMapLoggerForANNTraining.cpp b/src/propagation/BMapLoggerForANNTraining.cpp index dc73f9c6..406b50ae 100644 --- a/src/propagation/BMapLoggerForANNTraining.cpp +++ b/src/propagation/BMapLoggerForANNTraining.cpp @@ -79,9 +79,6 @@ BMapLoggerForANNTraining::BMapLoggerForANNTraining(const int & mindex, DataBroke BMapLoggerForANNTraining::~BMapLoggerForANNTraining() { - if (properties) { - delete[] properties; - } if (csvfile.is_open()) { csvfile.close(); } diff --git a/tests/unit/README.md b/tests/unit/README.md index c079bab2..1350a369 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -74,29 +74,26 @@ do not trip it. A pin moving means the model changed; that may well be intended, but it should be a decision rather than a surprise. The pins carry no claim of matching published values. -## Things found while writing these, and not fixed here - -Two of them are why `test_model_registry.cpp` only destroys the models that -register no properties. +## Things found while writing these **Models are never destroyed in a normal run.** `FireDomain` keeps them in `propModelsTable` and `fluxModelsTable` and frees neither, so every model a -simulation instantiates is leaked. That is why the two problems below have -never been observed: the code that would trip them does not run. - -**The `properties` array is deleted twice.** Seventeen flux models and two -propagation models delete `properties` in their own destructor, and -`~ForeFireModel` deletes it again. Most of them also use scalar `delete` on an -array allocated with `new[]`. So destroying any model that registers at least -one property is a double free. Fixing it means removing the `delete` from each -derived destructor and leaving it to the base class — nineteen files, worth -doing as its own change. - -`~ForeFireModel` itself was fixed while writing these tests: it left -`properties` uninitialised, so destroying a model that registers *no* -properties — `Iso`, `heatFluxBasic` — deleted whatever the member happened to -be built over. It also deleted `fuelPropertiesTable`, allocated with a scalar -`new`, with `delete[]`. +simulation instantiates is leaked. That is why the two memory bugs below went +unnoticed for so long: the code that trips them does not otherwise run. Still +open. + +**The `properties` array was deleted twice** — fixed, along with the test that +holds it fixed. Sixteen flux models and two propagation models deleted +`properties` in their own destructor while `~ForeFireModel` deleted it again, +and the flux ones used scalar `delete` on an array allocated with `new[]`. It is +allocated by each model's constructor and freed by the base class, so the +derived deletes are simply gone. `every model can be destroyed` covers all 33 +models; reintroduce one delete and it aborts with `double free or corruption`. + +`~ForeFireModel` itself left `properties` uninitialised, so destroying a model +that registers *no* properties — `Iso`, `heatFluxBasic` — deleted whatever the +member happened to be built over. It also freed `fuelPropertiesTable`, allocated +with a scalar `new`, using `delete[]`. Both fixed. **`BalbiNov2011` responds non-physically to live fuel moisture at the values in the shipped fuel table.** `xsi` exceeds 1 for fuel 1 of diff --git a/tests/unit/test_model_registry.cpp b/tests/unit/test_model_registry.cpp index 351b4b18..f735c5e8 100644 --- a/tests/unit/test_model_registry.cpp +++ b/tests/unit/test_model_registry.cpp @@ -116,30 +116,52 @@ TEST_CASE("an unknown model name is refused rather than fatal") { CHECK(flux == 0); } -TEST_CASE("a model with no properties can be destroyed") { - // Models that register no property never allocate their `properties` - // array, so they are the ones that expose whatever the base class leaves - // uninitialised: ~ForeFireModel deletes that pointer unconditionally. - // Iso is on the default path — it is what tests/python and test_wheel.py - // run — so this has to be safe. +TEST_CASE("every model can be destroyed") { + // `properties` is allocated by each model's own constructor with + // new double[numProperties] and freed by ~ForeFireModel. Freeing it in a + // derived destructor as well is a double free, and this case is what says + // so: run it under a build that reintroduces one and it aborts. // - // Nothing deletes a model in a normal run: FireDomain keeps them in - // propModelsTable and fluxModelsTable and never frees either, so the - // destructors below are reached only from here. That is also why this case - // covers only the property-less models: the ones that do allocate delete - // `properties` in their own destructor *and* inherit the base class doing - // it again, so destroying them is a double free. See tests/unit/README.md. + // Nothing deletes a model in a normal run — FireDomain keeps them in + // propModelsTable and fluxModelsTable and never frees either — so these + // destructors are reached only from here. That is precisely why the double + // free survived: the code that trips it does not otherwise run. ModelSandbox sandbox; - PropagationModel* iso = sandbox.propagation("Iso"); - REQUIRE(iso != 0); - REQUIRE(iso->numProperties == 0); - delete iso; + const std::vector& props = propagationModels(); + for (size_t i = 0; i < props.size(); i++) { + CAPTURE(props[i]); + PropagationModel* model = sandbox.propagation(props[i]); + REQUIRE(model != 0); + delete model; + } - FluxModel* heat = sandbox.flux("heatFluxBasic"); - REQUIRE(heat != 0); - REQUIRE(heat->numProperties == 0); - delete heat; + const std::vector& fluxes = fluxModels(); + for (size_t i = 0; i < fluxes.size(); i++) { + CAPTURE(fluxes[i]); + FluxModel* model = sandbox.flux(fluxes[i]); + REQUIRE(model != 0); + delete model; + } +} + +TEST_CASE("destroying a model does not disturb the next one") { + // A double free often shows up as the *next* allocation coming back + // corrupted rather than as an immediate abort, so allocate across the + // destruction and check the new model is intact. + ModelSandbox sandbox; + + PropagationModel* first = sandbox.propagation("Rothermel"); + REQUIRE(first != 0); + const std::vector wanted = first->wantedProperties; + const size_t count = first->numProperties; + delete first; + + PropagationModel* second = sandbox.propagation("Rothermel"); + REQUIRE(second != 0); + CHECK(second->numProperties == count); + CHECK(second->wantedProperties == wanted); + delete second; } TEST_CASE("property registration order is stable within a model") { From 4ecc171d217f7c3dcc6b3499adb15a0f6d19ae80 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Tue, 11 Aug 2026 14:30:48 +0200 Subject: [PATCH 06/63] fix(propagation): keep ROS finite and damped at high dead moisture Two rate-of-spread models misbehave once dead fuel moisture reaches or exceeds the moisture of extinction. Neither is reachable today, because Md is a static column of the fuel table that never gets that high, but both become reachable the moment dead fuel moisture is driven by a dynamic field. RothermelAndrews2018 returned NaN. Its moisture damping cubic is only defined below the moisture of extinction: it reaches zero at mf/me == 1 and turns negative past it, driving the reaction intensity negative, then the wind limit `wv = 0.9 * RI` negative, then pow(wv, B) to NaN for the non-integer B. The `R <= 0` exit test is false for NaN, so the value escaped into FireNode::velocity. Return zero before the cubic is evaluated, and negate the exit test so a NaN can no longer slip past it. The Balbi family sped up instead of slowing down. xsi is the share of combustion energy spent vaporising the moisture the live fuel carries in excess of the dead fuel; when the dead fuel is the wetter of the two it goes negative, so (1-xsi) exceeds 1 and amplifies both the radiant term and the flame temperature, which R00 raises to the fourth power. At Md=3.0 that put the flame temperature near 18000 K and made the fire spread 33x faster than at Md=1.0. Floor xsi at zero: the dead fuel's own penalty is already carried by the 1/(1 + a*Md) factor. Applied to all five models that share the expression. xsi is deliberately not capped above. With the current fuel tables it reaches ~5.3, which makes the flame temperature negative, but that traces back to Deltah equalling DeltaH in most rows of fuels.csv rather than to the model, and capping would move every existing calibration. Behaviour below the thresholds is unchanged to the last digit, and the runff KML and NetCDF references still match. --- src/propagation/Balbi2015.cpp | 4 ++++ src/propagation/BalbiNov2011.cpp | 7 +++++++ src/propagation/BalbiNov2011Curv.cpp | 4 ++++ src/propagation/BalbiNov2011TMdMl.cpp | 4 ++++ src/propagation/BalbiUnsteady.cpp | 4 ++++ src/propagation/RothermelAndrews2018.cpp | 13 ++++++++++++- 6 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/propagation/Balbi2015.cpp b/src/propagation/Balbi2015.cpp index 70b33eb2..7bc326f5 100644 --- a/src/propagation/Balbi2015.cpp +++ b/src/propagation/Balbi2015.cpp @@ -181,6 +181,10 @@ double Balbi2015::getSpeed(double* valueOf){ double r0 = lsd*lr00; double A0 = (lX0*lDeltaH)/(4*lCp*(lTi-lTa)); double xsi = ((lMl-lMd)*((lSigmal/lSigmad)*(lDeltah/lDeltaH))); + // See BalbiNov2011.cpp: a dead fuel wetter than the live fuel drives xsi + // negative, amplifying the radiant term and the flame temperature (R00 ~ T^4) + // instead of damping them. Wetter dead fuel must never speed a fire up. + if (xsi < 0.) xsi = 0.; double A = (nu*A0/(1+a*lMd))*(1-xsi); double T = lTa + (lDeltaH*(1-lX0)*(1-xsi))/((lstoch+1)*Cpa); double R00 = (B*T*T*T*T)/(lCp*(lTi-lTa)); diff --git a/src/propagation/BalbiNov2011.cpp b/src/propagation/BalbiNov2011.cpp index 6d8b457d..f668ff64 100644 --- a/src/propagation/BalbiNov2011.cpp +++ b/src/propagation/BalbiNov2011.cpp @@ -188,6 +188,13 @@ double BalbiNov2011::getSpeed(double* valueOf){ double A0 = (lX0*lDeltaH)/(4*lCp*(lTi-lTa)); /* double xsi = ((lMl-lMd)*((lSigmal/lSigmad)*(lDeltah/lDeltaH))); */ double xsi = ((lMl-lMd)*((Sd/Sl)*(lDeltah/lDeltaH))); // cf. Santoni et al., 2011 + // xsi is the share of the combustion energy spent vaporising the moisture + // the live fuel carries *in excess of* the dead fuel. When the dead fuel + // is the wetter of the two it goes negative, so (1-xsi) exceeds 1 and both + // the radiant term A and the flame temperature T are amplified rather than + // damped -- and R00 goes as T^4. Wetter dead fuel must never speed a fire + // up; its own penalty is already carried by the 1/(1 + a*Md) factor. + if (xsi < 0.) xsi = 0.; double A = cosCurv * (nu*A0 / (1 + a * lMd)) * (1-xsi); double T = lTa + ( lDeltaH*(1-lX0)*(1-xsi) ) / ((lstoch+1)*Cpa); double R00 = (B*T*T*T*T) / (lCp*(lTi-lTa)); diff --git a/src/propagation/BalbiNov2011Curv.cpp b/src/propagation/BalbiNov2011Curv.cpp index 1ab5a380..87f6411b 100644 --- a/src/propagation/BalbiNov2011Curv.cpp +++ b/src/propagation/BalbiNov2011Curv.cpp @@ -176,6 +176,10 @@ double BalbiNov2011Curv::getSpeed(double* valueOf){ double r0 = lsd * lr00; double A0 = (lX0*lDeltaH)/(4*lCp*(lTi-lTa)); double xsi = ((lMl-lMd)*((lSigmal/lSigmad)*(lDeltah/lDeltaH))); + // See BalbiNov2011.cpp: a dead fuel wetter than the live fuel drives xsi + // negative, amplifying the radiant term and the flame temperature (R00 ~ T^4) + // instead of damping them. Wetter dead fuel must never speed a fire up. + if (xsi < 0.) xsi = 0.; double A = (nu*A0 / (1 + a * lMd)) * (1-xsi); double T = lTa + ( lDeltaH*(1-lX0)*(1-xsi) ) / ((lstoch+1)*Cpa); double R00 = (B*T*T*T*T) / (lCp*(lTi-lTa)); diff --git a/src/propagation/BalbiNov2011TMdMl.cpp b/src/propagation/BalbiNov2011TMdMl.cpp index 957aecb6..700463b6 100644 --- a/src/propagation/BalbiNov2011TMdMl.cpp +++ b/src/propagation/BalbiNov2011TMdMl.cpp @@ -196,6 +196,10 @@ double BalbiNov2011TMdMl::getSpeed(double* valueOf){ double A0 = (lX0*lDeltaH)/(4*lCp*(lTi-lTa)); /* double xsi = ((lMl-lMd)*((lSigmal/lSigmad)*(lDeltah/lDeltaH))); */ double xsi = ((lMl-lMd)*((Sd/Sl)*(lDeltah/lDeltaH))); // cf. Santoni et al., 2011 + // See BalbiNov2011.cpp: a dead fuel wetter than the live fuel drives xsi + // negative, amplifying the radiant term and the flame temperature (R00 ~ T^4) + // instead of damping them. Wetter dead fuel must never speed a fire up. + if (xsi < 0.) xsi = 0.; double A = cosCurv * (nu*A0 / (1 + a * lMd)) * (1-xsi); double T = lTa + ( lDeltaH*(1-lX0)*(1-xsi) ) / ((lstoch+1)*Cpa); double R00 = (B*T*T*T*T) / (lCp*(lTi-lTa)); diff --git a/src/propagation/BalbiUnsteady.cpp b/src/propagation/BalbiUnsteady.cpp index 8fa3c1fc..59f921b8 100644 --- a/src/propagation/BalbiUnsteady.cpp +++ b/src/propagation/BalbiUnsteady.cpp @@ -176,6 +176,10 @@ double BalbiUnsteady::getSpeed(double* valueOf){ double r0 = lsd*lr00; double A0 = (lX0*lDeltaH)/(4*lCp*(lTi-lTa)); double xsi = ((lMl-lMd)*((lSigmal/lSigmad)*(lDeltah/lDeltaH))); + // See BalbiNov2011.cpp: a dead fuel wetter than the live fuel drives xsi + // negative, amplifying the radiant term and the flame temperature (R00 ~ T^4) + // instead of damping them. Wetter dead fuel must never speed a fire up. + if (xsi < 0.) xsi = 0.; double A = (nu*A0/(1+a*lMd))*(1-xsi); double T = lTa + (lDeltaH*(1-lX0)*(1-xsi))/((lstoch+1)*Cpa); double R00 = (B*T*T*T*T)/(lCp*(lTi-lTa)); diff --git a/src/propagation/RothermelAndrews2018.cpp b/src/propagation/RothermelAndrews2018.cpp index 859eabc9..926f6756 100644 --- a/src/propagation/RothermelAndrews2018.cpp +++ b/src/propagation/RothermelAndrews2018.cpp @@ -135,6 +135,15 @@ double RothermelAndrews2018::getSpeed(double* valueOf){ if (wv < 0) wv = 0; + // The moisture damping coefficient below is only defined for moisture + // contents under the moisture of extinction: its cubic reaches zero at + // mf/me == 1 and turns negative past it, which would drive the reaction + // intensity RI negative, then the wind limit `wv = 0.9 * RI` negative, + // then pow(wv, B) to NaN for the non-integer B. A NaN survives the + // `R <= 0` test below and reaches FireNode::velocity. Fuel at or above + // its moisture of extinction simply does not carry fire. + if (mf >= me) return 0; + if(wo > 0){ double Beta_op = 3.348 * pow(fpsa, -0.8189); // Optimum packing ratio double ODBD = wo / fd; // Ovendry bulk density @@ -162,7 +171,9 @@ double RothermelAndrews2018::getSpeed(double* valueOf){ double denominator = (ODBD * EHN * QIG); double R = numerator / denominator; // WC and SC will be zero at slope = wind = 0 - if(R <= 0.0) { + // Negated rather than `R <= 0.0` so that a NaN, which compares false + // against every bound, is caught here instead of being returned. + if(!(R > 0.0)) { return 0; }else{ return R * ftminToms; From f7cf7fde29fb29a7024dc2be3b699d30a45d6544 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Tue, 11 Aug 2026 14:31:03 +0200 Subject: [PATCH 07/63] test(propagation): add dead fuel moisture invariant suite The runff test compares ForeFire against frozen ForeFire output, so it cannot tell a physics fix from a physics regression: any deliberate change to a spread model breaks it, and any accidental one that leaves real_case.ff untouched slips through. This suite holds no reference data. Every assertion follows analytically from the published spread equations, so it stays valid across recalibration and across the planned switch from a static fuel.Md column to a dynamic dead-moisture field. Five invariants, over Rothermel, RothermelAndrews2018 and BalbiNov2011: finite ROS is a real number for every moisture in [0, 3] monotonic ROS decreases as dead fuel moisture rises extinction ROS reaches zero at Md >= me and stays there responsive a dynamic dead-moisture layer reaches the model resolved DataBroker finds an optimised getter for every property `responsive` skips until dynamic dead moisture exists, then activates. `resolved` exists because DataBroker does not raise on an unknown property: it warns and silently falls back to an un-optimised path, so a mistyped property name yields stale values rather than an error. Two constraints the probe has to respect. Each probe runs in its own interpreter, because the core keeps mutable global state and a second ForeFire() in one process inherits the first one's parameters -- an in-process sweep returns one identical displacement for every input. And relax is forced to 1, because below that FireNode::update blends in the ignition velocity, which decays geometrically but never reaches zero, so an extinguished front still creeps and extinction is unobservable. Each assertion was mutation-checked: flipping the sign of the moisture damping term, applying the dead-moisture layer inverted, removing the moisture-of-extinction cutoff and mistyping a property name each turn the matching invariant red. Runs in about 16 seconds. --- tests/python/test_moisture_invariants.py | 559 +++++++++++++++++++++++ 1 file changed, 559 insertions(+) create mode 100644 tests/python/test_moisture_invariants.py diff --git a/tests/python/test_moisture_invariants.py b/tests/python/test_moisture_invariants.py new file mode 100644 index 00000000..105dea3f --- /dev/null +++ b/tests/python/test_moisture_invariants.py @@ -0,0 +1,559 @@ +#!/usr/bin/env python3 +"""Invariant tests for dead fuel moisture in ForeFire propagation models. + +Unlike ``tests/runff``, this suite holds no reference output. Every assertion +here is a property that follows analytically from the published spread +equations, so it stays valid across recalibration, refactoring, and the +planned switch from a static ``fuel.Md`` column to a dynamic dead-moisture +field. A test that compares against frozen ForeFire output cannot tell a fix +from a regression; these can. + +Invariants +---------- +finite ROS is a real number for every moisture in [0, 3]. Never NaN. +monotonic ROS decreases as dead fuel moisture rises, all else equal. +extinction ROS reaches 0 at Md >= me, for models that define a moisture + of extinction, and stays 0 above it. +responsive A dynamic dead-moisture layer actually reaches the model + (skipped until dynamic Md exists -- see PLUMBING below). +resolved DataBroker finds an optimised getter for every property the + model registers, i.e. no silent un-optimised fallback. + +Why ROS is probed through a simulation +-------------------------------------- +``PropagationModel::getSpeed`` is not exposed to Python, so each probe runs a +short spread from a point ignition over uniform fuel, flat ground and uniform +wind, then measures how far the front travelled. Front displacement over a +fixed duration is monotone in ROS, and works for every propagation model +without knowing its parameter set. + +Why every probe forks +--------------------- +The C++ core keeps mutable global state -- FireDomain's model registries and +SimulationParameters, as ``pyproject.toml`` notes when explaining why the +free-threaded build is skipped. A second ``ForeFire()`` in the same process +inherits the first one's parameters, and the whole sweep then returns one +identical displacement regardless of moisture. Running the sweep in-process +produces a test that passes or fails for reasons unrelated to its assertions, +so each probe is executed in a fresh interpreter via ``--probe``. + +``minSpeed`` is forced to 0. At its default of 0.005 m/s (see +src/SimulationParameters.cpp:378) FireNode::update refuses to move a node +below the floor (src/FireNode.cpp:205), which would mask genuine extinction +behind the same zero displacement. + +NaN detection +------------- +A NaN ROS does *not* show up as a NaN displacement: ``speed > minSpeed`` is +false for NaN, so the node stops and the fire merely looks extinguished. The +finiteness check therefore scans the raw ``print[]`` text, because +``FireNode::toString`` (src/FireNode.cpp:750) emits ``vel=`` from a velocity +that is assigned unconditionally at src/FireNode.cpp:201, before the +``minSpeed`` gate. A NaN speed reaches the printed output. + +PLUMBING +-------- +``DataBroker`` does not raise on an unknown property name. It prints +"WARNING: could not find an optimized property getter for ..." and falls back +to an un-optimised path (src/DataBroker.cpp:161-170), so a typo in a +registered property name yields stale values rather than an error. The +``resolved`` test asserts that warning never appears. + +Usage +----- + python3 tests/python/test_moisture_invariants.py # run all + python3 tests/python/test_moisture_invariants.py -v # per-probe ROS +""" + +import argparse +import ctypes +import io +import json +import math +import os +import re +import subprocess +import sys +import tempfile +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager + +# -------------------------------------------------------------------------- +# Fuel tables +# +# One burnable row, everything but the moisture column held fixed, so a sweep +# varies exactly one input. Values are taken from the Mediterranean shrub row +# (index 82) of tests/runff/fuels.csv and from the SH fuel group of +# pyforefire.helpers.RothermelAndrews2018FuelTable. +# -------------------------------------------------------------------------- + +FUEL_INDEX = 82 + +_BALBI_HEADER = ( + "Index;Rhod;Rhol;Md;Ml;sd;sl;e;Sigmad;Sigmal;stoch;RhoA;Ta;Tau0;" + "Deltah;DeltaH;Cp;Cpa;Ti;X0;r00;Blai;me" +) + +_ANDREWS_HEADER = ( + "Index;fl1h_tac;fd_ft;Dme_pc;SAVcar_ftinv;H_BTUlb;fuelDens_lbft3;" + "totMineral_r;effectMineral_r;mdOnDry1h_r" +) + + +def balbi_table(md, me=0.30): + """Rothermel / Balbi column set. Moisture is a dimensionless ratio.""" + row = ( + f"{FUEL_INDEX};614.0;613.0;{md!r};1.0;4287.0;5738.0;0.4;1.378;0.174;" + f"8.3;1.0;300;70000;18727000.0;18727000.0;1800;1000;600;0.3;" + f"2.5e-05;4.0;{me!r}" + ) + return _BALBI_HEADER + "\n" + row + + +def andrews_table(md, me=0.30): + """RothermelAndrews2018 column set. `Dme_pc` is a percentage, not a ratio.""" + row = ( + f"{FUEL_INDEX};1.74;1.0;{me * 100.0!r};1550.0;8000.0;32.0;" + f"0.0555;0.010;{md!r}" + ) + return _ANDREWS_HEADER + "\n" + row + + +class ModelSpec: + """A propagation model plus what its equations promise about moisture.""" + + def __init__(self, name, table, has_extinction): + self.name = name + self.table = table + # True when the model defines a moisture of extinction above which + # ROS is exactly zero. Balbi has no such threshold: its ROS decays + # smoothly with Md and never reaches zero analytically. + self.has_extinction = has_extinction + + def __repr__(self): + return f"<{self.name}>" + + +MODELS = ( + ModelSpec("Rothermel", balbi_table, has_extinction=True), + ModelSpec("RothermelAndrews2018", andrews_table, has_extinction=True), + ModelSpec("BalbiNov2011", balbi_table, has_extinction=False), +) + +ME = 0.30 # moisture of extinction used throughout + +# -------------------------------------------------------------------------- +# Probe +# -------------------------------------------------------------------------- + +DOMAIN = 2000.0 # m, square +GRID = 100 # cells per side +WIND = 2.0 # m/s, along +x +DURATION = 120.0 # s -- short enough that the driest fuel stays off the edge +IGNITION = (DOMAIN / 2.0, DOMAIN / 2.0) + +# Moisture far above any model's extinction threshold. Its displacement is +# the "did not spread" floor: a point ignition always lays down an initial +# front of finite size before the first update, so extinction shows up as +# displacement equal to this floor, not as exactly zero. +FLOOR_MD = 10.0 +FLOOR_TOL = 1.05 # 5% slack over the floor still counts as extinguished + +_SENTINEL = "@@PROBE@@" + +_LOC_RE = re.compile(r"loc=\(\s*([-+0-9.eEnaif]+)\s*,\s*([-+0-9.eEnaif]+)") +_NONFINITE_RE = re.compile(r"-?\b(nan|inf|Inf|NaN|IND)\b", re.IGNORECASE) + + +@contextmanager +def captured_native_output(): + """Capture stdout written by the C++ core, not just by Python. + + ForeFire's warnings go to std::cout inside the extension module, which + never passes through sys.stdout. Redirecting requires the file + descriptor, so dup2 onto a temp file and restore afterwards. + """ + libc = ctypes.CDLL(None) + saved = os.dup(1) + with tempfile.TemporaryFile(mode="w+b") as tmp: + try: + sys.stdout.flush() # Python's own buffer; fflush only covers libc + libc.fflush(None) + os.dup2(tmp.fileno(), 1) + buf = io.StringIO() + yield buf + finally: + sys.stdout.flush() + libc.fflush(None) + os.dup2(saved, 1) + os.close(saved) + tmp.seek(0) + buf.write(tmp.read().decode("utf-8", "replace")) + + +class Probe: + """Result of one spread run.""" + + def __init__(self, distance, raw, native): + self.distance = distance # m travelled by the furthest front node + self.raw = raw # concatenated print[] output + self.native = native # stdout emitted by the C++ core + + @property + def ros(self): + """Mean ROS along the fastest ray, m/s.""" + return self.distance / DURATION + + @property + def has_nonfinite(self): + return bool(_NONFINITE_RE.search(self.raw)) + + +def probe(model, md, *, me=ME, wind=WIND, dead_moisture_layer=None): + """Spread a fire for DURATION seconds and report how far it got. + + Runs in a forked interpreter; see "Why every probe forks" above. + + `dead_moisture_layer`, when given, is a constant added as a `deadMoisture` + scalar layer. It is ignored by the current code -- that is exactly what + the `responsive` test detects. + """ + spec = { + "model": model.name, + "md": md, + "me": me, + "wind": wind, + "duration": DURATION, + "dead_moisture_layer": dead_moisture_layer, + } + proc = subprocess.run( + [sys.executable, os.path.abspath(__file__), "--probe", json.dumps(spec)], + capture_output=True, text=True, timeout=300, + ) + for line in proc.stdout.splitlines(): + if line.startswith(_SENTINEL): + payload = json.loads(line[len(_SENTINEL):]) + return Probe(payload["distance"], payload["raw"], payload["native"]) + raise RuntimeError( + f"probe subprocess produced no result (exit {proc.returncode})\n" + f"stdout: {proc.stdout[-2000:]}\nstderr: {proc.stderr[-2000:]}" + ) + + +def probe_many(model, mds, **kw): + """Run a moisture sweep, one subprocess per point, in parallel.""" + with ThreadPoolExecutor(max_workers=min(8, (os.cpu_count() or 2))) as pool: + return list(pool.map(lambda md: probe(model, md, **kw), mds)) + + +def _probe_in_process(spec): + """The actual simulation. Only ever called in a freshly forked child.""" + import numpy as np + import pyforefire as forefire + + model = next(m for m in MODELS if m.name == spec["model"]) + md, me = spec["md"], spec["me"] + wind, duration = spec["wind"], spec["duration"] + dead_moisture_layer = spec["dead_moisture_layer"] + + ff = forefire.ForeFire() + + ff["fuelsTable"] = model.table(md, me) + ff["propagationModel"] = model.name + + # No lower clamp: extinction has to be observable as zero displacement. + ff["minSpeed"] = 0.0 + ff["windReductionFactor"] = 1.0 + ff["propagationSpeedAdjustmentFactor"] = 1.0 + + # Front-tracking numerics. Fixed across the sweep so that any change in + # displacement is attributable to moisture alone. + ff["spatialIncrement"] = 1.0 + ff["perimeterResolution"] = 15.0 + ff["minimalPropagativeFrontDepth"] = 20.0 + ff["initialFrontDepth"] = 5.0 + # relax=1 makes FireNode::update take the model's speed directly + # (src/FireNode.cpp:195). Any relaxation below 1 blends in the ignition + # velocity, which decays geometrically but never reaches zero, so a fully + # extinguished front still creeps one spatialIncrement per step and + # extinction becomes unobservable. + ff["relax"] = 1.0 + ff["smoothing"] = 0 + ff["bmapLayer"] = 1 + ff["defaultHeatType"] = 0 + ff["nominalHeatFlux"] = 100000 + ff["burningDuration"] = 100 + + ff["SWx"] = 0.0 + ff["SWy"] = 0.0 + ff["Lx"] = DOMAIN + ff["Ly"] = DOMAIN + ff["atmoNX"] = GRID + ff["atmoNY"] = GRID + + fuel_map = np.full((1, 1, GRID, GRID), FUEL_INDEX, dtype=np.int32) + zeros = np.zeros((1, 2, GRID, GRID)) + ones = np.zeros((1, 2, GRID, GRID)) + ones[0, 0, :, :] = 1.0 + + with captured_native_output() as native: + ff.execute( + f"FireDomain[sw=(0.,0.,0.);ne=({DOMAIN},{DOMAIN},0);t=0]" + ) + ff.addLayer("propagation", model.name, "propagationModel") + ff.addIndexLayer( + "table", "fuel", 0.0, 0.0, 0, DOMAIN, DOMAIN, 0, fuel_map + ) + ff.addScalarLayer( + "windScalDir", "windU", 0.0, 0.0, 0, DOMAIN, DOMAIN, 0, ones + ) + ff.addScalarLayer( + "windScalDir", "windV", 0.0, 0.0, 0, DOMAIN, DOMAIN, 0, zeros + ) + if dead_moisture_layer is not None: + layer = np.full((1, 1, GRID, GRID), float(dead_moisture_layer)) + ff.addScalarLayer( + "data", "deadMoisture", 0.0, 0.0, 0, DOMAIN, DOMAIN, 0, layer + ) + + ff.execute(f"trigger[wind;loc=(0.,0.,0.);vel=({wind},0.,0.);t=0]") + ff.execute( + f"startFire[loc=({IGNITION[0]},{IGNITION[1]},0.);t=0.]" + ) + + raw = ff.execute("print[]") + ff.execute(f"goTo[t={duration}]") + raw += ff.execute("print[]") + + return { + "distance": _max_displacement(raw), + "raw": raw, + "native": native.getvalue(), + } + + +def _max_displacement(raw): + """Furthest distance any front node reached from the ignition point.""" + best = 0.0 + for xs, ys in _LOC_RE.findall(raw): + try: + x, y = float(xs), float(ys) + except ValueError: + continue # a non-finite coordinate; has_nonfinite reports it + if not (math.isfinite(x) and math.isfinite(y)): + continue + best = max(best, math.hypot(x - IGNITION[0], y - IGNITION[1])) + return best + + +# -------------------------------------------------------------------------- +# Invariants +# -------------------------------------------------------------------------- + +# Sampled well below the moisture of extinction, where every model is smooth +# and strictly decreasing. +DRY_SWEEP = (0.02, 0.06, 0.10, 0.14, 0.18, 0.22, 0.28) + +# At and above the moisture of extinction. Includes values a dynamic +# dead-moisture field produces routinely after rain -- 1.0 is 100% moisture +# on a dry-weight basis, ordinary for live-adjacent litter, and 3.0 is the +# kind of value a saturated-fuel parameterisation can emit. +WET_SWEEP = (0.30, 0.32, 0.50, 1.00, 3.00) + + +def test_finite(model, report): + """ROS is a real number for every moisture, on both sides of `me`.""" + failures = [] + sweep = DRY_SWEEP + WET_SWEEP + for md, p in zip(sweep, probe_many(model, sweep)): + report(f" Md={md:<5} d={p.distance:8.2f} m ROS={p.ros:.4f} m/s") + if p.has_nonfinite: + failures.append( + f"Md={md}: non-finite value in front output " + f"(NaN/inf ROS reaches FireNode::velocity)" + ) + if not math.isfinite(p.distance): + failures.append(f"Md={md}: non-finite front displacement") + return failures + + +def test_monotonic(model, report): + """ROS decreases as dead fuel moisture rises, all else equal. + + Holds analytically for Rothermel: the damping polynomial + 1 - 2.59x + 5.11x^2 - 3.52x^3 has derivative -2.59 + 10.22x - 10.56x^2, + whose discriminant is -4.95, so it is negative everywhere; and the heat + of preignition Qig = 250 + 1116*Md sits in the denominator. Both terms + push the same way. Balbi's 1/(1 + a*Md) factor does likewise. + """ + failures = [] + sweep = DRY_SWEEP + WET_SWEEP + results = probe_many(model, sweep) + + prev_md, prev = None, None + for md, p in zip(sweep, results): + report(f" Md={md:<5} d={p.distance:8.2f} m ROS={p.ros:.4f} m/s") + if prev is not None: + # Below the moisture of extinction every model is smooth and + # strictly decreasing. At and above it, models with a threshold + # plateau at zero, so only require non-increasing there. + strict = md <= DRY_SWEEP[-1] + violated = ( + p.distance >= prev.distance if strict + else p.distance > prev.distance + ) + if violated: + failures.append( + f"ROS {'did not decrease' if strict else 'increased'} " + f"from Md={prev_md} to Md={md}: " + f"{prev.distance:.2f} m -> {p.distance:.2f} m" + ) + prev_md, prev = md, p + return failures + + +def test_extinction(model, report): + """At and above the moisture of extinction the fire must not spread.""" + if not model.has_extinction: + report(" skipped: model defines no moisture of extinction") + return [] + + failures = [] + floor = probe(model, FLOOR_MD).distance + limit = floor * FLOOR_TOL + report(f" Md={FLOOR_MD:<5} d={floor:8.2f} m (floor: initial front only)") + + control_md = ME * 0.9 + control = probe(model, control_md) + report(f" Md={control_md:<5} d={control.distance:8.2f} m (control, must burn)") + if control.distance <= limit: + failures.append( + f"control at Md={control_md} did not outrun the floor " + f"({control.distance:.2f} m vs {floor:.2f} m); the assertions " + f"below would pass vacuously" + ) + + for md, p in zip(WET_SWEEP, probe_many(model, WET_SWEEP)): + report(f" Md={md:<5} d={p.distance:8.2f} m (must not spread)") + if p.distance > limit: + failures.append( + f"spread {p.distance:.2f} m at Md={md} >= me={ME} " + f"(floor is {floor:.2f} m)" + ) + return failures + + +def test_responsive(model, report): + """A dynamic dead-moisture layer must actually reach the model. + + Skips until dynamic dead fuel moisture exists. Once `deadMoisture` is a + registered property, a layer far above the moisture of extinction must + stop a fire whose fuel-table Md says it should burn. + """ + dry = probe(model, 0.05) + forced = probe(model, 0.05, dead_moisture_layer=1.0) + report( + f" table Md=0.05 d={dry.distance:8.2f} m\n" + f" + deadMoisture=1.0 d={forced.distance:8.2f} m" + ) + if abs(forced.distance - dry.distance) < 1e-9: + report(" skipped: deadMoisture layer is ignored (not implemented yet)") + return [] + if not model.has_extinction: + return [] + # Same floor as test_extinction: a point ignition always lays down an + # initial front, so "stopped" means "no further than the floor". + floor = probe(model, FLOOR_MD).distance + if forced.distance > floor * FLOOR_TOL: + return [ + f"deadMoisture layer at 1.0 (>> me={ME}) did not stop the fire: " + f"spread {forced.distance:.2f} m (floor is {floor:.2f} m)" + ] + return [] + + +def test_resolved(model, report): + """Every registered property resolves to an optimised getter. + + src/DataBroker.cpp:161-170 warns and silently degrades instead of + failing, so a mistyped property name would otherwise go unnoticed. + """ + p = probe(model, 0.10) + failures = [] + for marker in ( + "could not find an optimized property getter", + "switched to an un-optimized mode", + ): + if marker in p.native: + failures.append(f"DataBroker fell back: {marker!r}") + report(" no DataBroker fallback warnings" if not failures else "") + return failures + + +TESTS = ( + ("finite", test_finite), + ("monotonic", test_monotonic), + ("extinction", test_extinction), + ("responsive", test_responsive), + ("resolved", test_resolved), +) + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("-v", "--verbose", action="store_true", + help="print every probe's displacement and ROS") + ap.add_argument("--model", action="append", metavar="NAME", + help="restrict to these propagation models") + ap.add_argument("--test", action="append", metavar="NAME", + help="restrict to these invariants") + ap.add_argument("--probe", metavar="JSON", + help=argparse.SUPPRESS) # internal: run one probe and exit + args = ap.parse_args(argv) + + if args.probe: + result = _probe_in_process(json.loads(args.probe)) + print(_SENTINEL + json.dumps(result)) + return 0 + + models = MODELS + if args.model: + models = tuple(m for m in MODELS if m.name in args.model) + if not models: + ap.error(f"no such model; known: {[m.name for m in MODELS]}") + tests = TESTS + if args.test: + tests = tuple(t for t in TESTS if t[0] in args.test) + if not tests: + ap.error(f"no such test; known: {[t[0] for t in TESTS]}") + + def report(msg): + if args.verbose and msg: + print(msg) + + total = 0 + for model in models: + print(f"\n=== {model.name} ===") + for name, fn in tests: + print(f" {name}") + try: + failures = fn(model, report) + except Exception as exc: # a crash is a failure, not an error + failures = [f"raised {type(exc).__name__}: {exc}"] + if failures: + total += len(failures) + for f in failures: + print(f" FAIL: {f}") + else: + print(" ok") + + print() + if total: + print(f"FAILED: {total} invariant violation(s)") + return 1 + print("All invariants hold.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 81089ea26bbf0649c2dd53d6b749efab7b1c7621 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Tue, 11 Aug 2026 14:43:04 +0200 Subject: [PATCH 08/63] fix(databroker): stop "moisture" being served the five-slot getter registerPropagationModel matched the prefix "moist" and routed anything starting with it to getMoisturesProperties, which fills five slots from the moistures.* scalar parameters. That getter exists for the NFDRS-style group FarsitePropagationModel registers as moist.ones / moist.tens / moist.hundreds / moist.liveh / moist.livew, where five reserved slots match five written values. The prefix also caught the singular "moisture", which reserves one slot. Balbi2015, Balbi2020 and BalbiNov2011TMdMl each register it, so every property they registered afterwards was written four slots too late and the last four ran past the end of the properties array. Balbi2020 read its temperature out of moistures.liveh, its Rhod out of moistures.tens and its Md out of moistures.hundreds, and wrote 28 doubles into a 24-element allocation. Measured, it spread the same 42.43 m -- the distance covered by the initial front alone -- at every fuel-table Md from 0.02 to 0.40: the model could not see its own fuel table. Match "moist." instead, so the group still reaches the five-slot getter while "moisture" resolves through propPropertiesGetters to getMoisture, which writes exactly one value. That leaves getMoisture reachable for those three models for the first time, and it dereferenced moistureLayer unconditionally. Guard both overloads and fall back to the moistures.ones parameter, which is the value these models were already receiving, so no configured simulation changes its numbers; warn once, since a model asking for a moisture field and getting a constant is almost certainly misconfigured. The runff KML and NetCDF references are unaffected: Rothermel registers no moisture property. --- src/DataBroker.cpp | 40 +++++++++++++++++++++++++++++++++++++--- src/DataBroker.h | 2 ++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/DataBroker.cpp b/src/DataBroker.cpp index 5a70fd00..45e3a790 100644 --- a/src/DataBroker.cpp +++ b/src/DataBroker.cpp @@ -147,7 +147,16 @@ namespace libforefire } } else - if ((model->wantedProperties)[prop].substr(0, 5) == "moist") + // Only the "moist." group, i.e. the five NFDRS-style dead and + // live moistures FarsitePropagationModel registers as + // moist.ones / moist.tens / moist.hundreds / moist.liveh / + // moist.livew. getMoisturesProperties fills five slots, so it + // is only correct for a model that reserved five of them. + // The shorter prefix "moist" also caught the singular + // "moisture", which reserves one slot, so every property a + // model registered after it was written four slots too late + // -- and the last four ran off the end of the array. + if ((model->wantedProperties)[prop].substr(0, 6) == "moist.") { if (!moistAsked) { @@ -818,9 +827,33 @@ namespace libforefire return 5; } + /*! \brief moisture to use when a model asks for it but no layer supplies it + + * Returns the moistures.ones parameter, which is the value such a model + * received before the "moist" prefix fix, so no simulation silently + * changes its numbers. Warns once, because a model that wants a moisture + * field and is given a constant is almost certainly misconfigured. + */ + double DataBroker::fallbackMoisture() + { + static bool warned = false; + double m = params->isValued("moistures.ones") + ? params->getDouble("moistures.ones") + : 0.032; + if (!warned) + { + warned = true; + cout << "WARNING: a model requires the 'moisture' property but no " + "moisture layer was loaded; falling back to the constant " + "moistures.ones = " << m << endl; + } + return m; + } + int DataBroker::getMoisture(FireNode *fn, PropagationModel *model, int keynum) { - (model->properties)[keynum] = moistureLayer->getValueAt(fn); + (model->properties)[keynum] = + moistureLayer ? moistureLayer->getValueAt(fn) : fallbackMoisture(); return 1; } @@ -933,7 +966,8 @@ namespace libforefire int DataBroker::getMoisture(FFPoint loc, const double &t, FluxModel *model, int keynum) { - (model->properties)[keynum] = moistureLayer->getValueAt(loc, t); + (model->properties)[keynum] = + moistureLayer ? moistureLayer->getValueAt(loc, t) : fallbackMoisture(); return 1; } diff --git a/src/DataBroker.h b/src/DataBroker.h index d5901a38..b54c0281 100644 --- a/src/DataBroker.h +++ b/src/DataBroker.h @@ -146,6 +146,8 @@ class DataBroker { static int getDummy(FireNode*, PropagationModel*, int); /*! \brief predefined function for getting the fuel parameters at firenode location */ static int getFuelProperties(FireNode*, PropagationModel*, int); + /*! \brief constant moisture used when no moisture layer is loaded */ + static double fallbackMoisture(); /*! \brief predefined function for getting the moisture at firenode location */ static int getMoisturesProperties(FireNode *, PropagationModel *, int ); From 5f9d520cdc77ef5db59bd0aceb80d2827beed7f6 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Tue, 11 Aug 2026 14:43:16 +0200 Subject: [PATCH 09/63] test(propagation): cover the models that register "moisture" Balbi2020, Balbi2015 and BalbiNov2011TMdMl each register a "moisture" property beside their fuel properties, which is what the property-shift fix addresses. Supply a temperature and a moisture layer from the probe so all three can run, and add them to the sweep. Reverting the "moist." prefix narrowing turns Balbi2020's monotonicity red, so the fix is now covered. Run duration is calibrated per model rather than fixed. Spread rates differ by more than two orders of magnitude across these six models, and one duration either drives the fast ones into the domain edge, clipping the dry end of the sweep, or leaves the slow ones so close to the initial front that perimeterResolution quantises the displacement and adjacent moistures collide. The calibration iterates, because a Balbi front accelerates as it develops and a rate measured over a short trial underestimates the eventual one. All six models now land between 350 and 715 m in a 2000 m domain, well clear of both failure modes, and the sweep keeps resolving if any model is recalibrated later. Runs in about 40 seconds. --- tests/python/test_moisture_invariants.py | 83 ++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/tests/python/test_moisture_invariants.py b/tests/python/test_moisture_invariants.py index 105dea3f..8ee7f9af 100644 --- a/tests/python/test_moisture_invariants.py +++ b/tests/python/test_moisture_invariants.py @@ -138,9 +138,18 @@ def __repr__(self): ModelSpec("Rothermel", balbi_table, has_extinction=True), ModelSpec("RothermelAndrews2018", andrews_table, has_extinction=True), ModelSpec("BalbiNov2011", balbi_table, has_extinction=False), + # These three register "moisture" alongside their fuel properties. Until + # the "moist" prefix in registerPropagationModel was narrowed to "moist.", + # that single registration was served by the five-slot + # getMoisturesProperties, shifting every later property by four and + # leaving each model unable to see its own fuel table at all. + ModelSpec("Balbi2020", balbi_table, has_extinction=False), + ModelSpec("Balbi2015", balbi_table, has_extinction=False), + ModelSpec("BalbiNov2011TMdMl", balbi_table, has_extinction=False), ) ME = 0.30 # moisture of extinction used throughout +LIVE_MOISTURE = 1.0 # matches the Ml column of the tables above # -------------------------------------------------------------------------- # Probe @@ -149,9 +158,24 @@ def __repr__(self): DOMAIN = 2000.0 # m, square GRID = 100 # cells per side WIND = 2.0 # m/s, along +x -DURATION = 120.0 # s -- short enough that the driest fuel stays off the edge IGNITION = (DOMAIN / 2.0, DOMAIN / 2.0) +# Run duration is calibrated per model rather than fixed. Spread rates +# differ by more than an order of magnitude between models, and a single +# duration either drives the fast ones off the domain edge -- clipping the +# dry end of the sweep -- or leaves the slow ones barely clear of the +# initial front, where perimeterResolution quantises the displacement and +# adjacent moistures collide. Calibrating keeps every model in the range +# where displacement actually resolves ROS, and survives recalibration of +# any model without anyone editing this file. +CALIB_DURATION = 60.0 # s, the trial run used to estimate ROS +TARGET_SPREAD = 600.0 # m, comfortably inside the 1000 m half-width +MIN_SPREAD = 300.0 # m, below this adjacent moistures start to collide +MAX_SPREAD = 800.0 # m, above this the front is closing on the domain edge +MIN_DURATION = 5.0 # s, floor for a very fast model +MAX_DURATION = 3600.0 # s, ceiling for a very slow model +CALIB_ROUNDS = 4 # Balbi fronts accelerate, so one round can undershoot + # Moisture far above any model's extinction threshold. Its displacement is # the "did not spread" floor: a point ignition always lays down an initial # front of finite size before the first update, so extinction shows up as @@ -194,23 +218,53 @@ def captured_native_output(): class Probe: """Result of one spread run.""" - def __init__(self, distance, raw, native): + def __init__(self, distance, raw, native, duration): self.distance = distance # m travelled by the furthest front node self.raw = raw # concatenated print[] output self.native = native # stdout emitted by the C++ core + self.duration = duration # s of simulated time @property def ros(self): """Mean ROS along the fastest ray, m/s.""" - return self.distance / DURATION + return self.distance / self.duration @property def has_nonfinite(self): return bool(_NONFINITE_RE.search(self.raw)) -def probe(model, md, *, me=ME, wind=WIND, dead_moisture_layer=None): - """Spread a fire for DURATION seconds and report how far it got. +_DURATIONS = {} + + +def duration_for(model): + """Pick a run duration that puts this model's driest case near TARGET_SPREAD. + + Iterated rather than solved in one step: a Balbi front accelerates as it + develops, so ROS measured over a short trial underestimates the eventual + rate and the first duration overshoots into the domain edge. + """ + if model.name in _DURATIONS: + return _DURATIONS[model.name] + + floor = probe(model, FLOOR_MD, duration=CALIB_DURATION).distance + duration = CALIB_DURATION + for _ in range(CALIB_ROUNDS): + reached = probe(model, DRY_SWEEP[0], duration=duration).distance + if MIN_SPREAD <= reached <= MAX_SPREAD: + break + ros = max((reached - floor) / duration, 1e-6) + # Clamped to MIN_DURATION, not CALIB_DURATION: a model fast enough to + # saturate the domain inside the trial needs a *shorter* run than the + # trial, and clamping at the trial length would leave it clipped. + duration = min(max(TARGET_SPREAD / ros, MIN_DURATION), MAX_DURATION) + + _DURATIONS[model.name] = duration + return duration + + +def probe(model, md, *, me=ME, wind=WIND, duration=None, dead_moisture_layer=None): + """Spread a fire and report how far the furthest front node got. Runs in a forked interpreter; see "Why every probe forks" above. @@ -218,12 +272,14 @@ def probe(model, md, *, me=ME, wind=WIND, dead_moisture_layer=None): scalar layer. It is ignored by the current code -- that is exactly what the `responsive` test detects. """ + if duration is None: + duration = duration_for(model) spec = { "model": model.name, "md": md, "me": me, "wind": wind, - "duration": DURATION, + "duration": duration, "dead_moisture_layer": dead_moisture_layer, } proc = subprocess.run( @@ -233,7 +289,8 @@ def probe(model, md, *, me=ME, wind=WIND, dead_moisture_layer=None): for line in proc.stdout.splitlines(): if line.startswith(_SENTINEL): payload = json.loads(line[len(_SENTINEL):]) - return Probe(payload["distance"], payload["raw"], payload["native"]) + return Probe(payload["distance"], payload["raw"], payload["native"], + duration) raise RuntimeError( f"probe subprocess produced no result (exit {proc.returncode})\n" f"stdout: {proc.stdout[-2000:]}\nstderr: {proc.stderr[-2000:]}" @@ -310,6 +367,18 @@ def _probe_in_process(spec): ff.addScalarLayer( "windScalDir", "windV", 0.0, 0.0, 0, DOMAIN, DOMAIN, 0, zeros ) + # Models such as Balbi2020 register "temperature" and "moisture" + # (the latter meaning live fuel moisture) beside their fuel + # properties. Supplying both unconditionally keeps every model on the + # same footing; models that do not register them never read them. + ff.addScalarLayer( + "data", "temperature", 0.0, 0.0, 0, DOMAIN, DOMAIN, 0, + np.full((1, 1, GRID, GRID), 300.0), + ) + ff.addScalarLayer( + "data", "moisture", 0.0, 0.0, 0, DOMAIN, DOMAIN, 0, + np.full((1, 1, GRID, GRID), LIVE_MOISTURE), + ) if dead_moisture_layer is not None: layer = np.full((1, 1, GRID, GRID), float(dead_moisture_layer)) ff.addScalarLayer( From 3fb92a5b52630657efd8fb3380c2c2daf9ab3731 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Tue, 11 Aug 2026 14:58:44 +0200 Subject: [PATCH 10/63] ci: run the model invariant suite on Linux Adds invariants.yml, which builds the pyforefire extension and runs tests/python/test_moisture_invariants.py on pushes to master, pull requests against it, and manual dispatch. A separate workflow rather than a job in main.yml, because the suite needs the Python extension and install-forefire.sh does not build it: outside a wheel build CMakeLists defaults FOREFIRE_BUILD_PYTHON to OFF, so main.yml produces only the native interpreter. The job installs the package into a venv instead of system Python, which keeps it clear of PEP 668, and pip's build isolation supplies scikit-build-core and pybind11 from pyproject.toml. Checkout skips LFS, unlike main.yml: the suite builds its fuel, wind, temperature and moisture layers in memory and needs no fixtures. Build and suite together take about a minute locally. This gives the repository a check that can distinguish a physics fix from a physics regression. The runff test compares ForeFire against frozen ForeFire output, so a deliberate model change breaks it while an accidental one that leaves real_case.ff untouched passes. Also documents the suite in TESTING.md, including the requirement that each probe run in its own interpreter, and corrects the claim there that nothing under tests/python is validated in CI. --- .github/workflows/invariants.yml | 53 ++++++++++++++++++++++++++++++++ README.md | 1 + TESTING.md | 37 +++++++++++++++++++++- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/invariants.yml diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml new file mode 100644 index 00000000..90760285 --- /dev/null +++ b/.github/workflows/invariants.yml @@ -0,0 +1,53 @@ +name: 🔬 Model Invariants (Linux) + +# Complements the runff test in main.yml. That one compares ForeFire against +# frozen ForeFire output, so it cannot distinguish a physics fix from a +# physics regression. This one asserts properties that follow from the +# published spread equations and holds no reference data. +# +# It is a separate workflow because it needs the pyforefire extension, which +# install-forefire.sh does not build: outside a wheel build, CMakeLists +# defaults FOREFIRE_BUILD_PYTHON to OFF. + +on: + push: + branches: + - "master" + pull_request: + branches: [ "master" ] + workflow_dispatch: + +jobs: + invariants: + name: Dead fuel moisture invariants + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + # No `lfs: true` here, unlike main.yml. The suite builds its fuel, wind, + # temperature and moisture layers in memory and needs no fixtures. + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install Dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends \ + build-essential cmake python3 python3-pip python3-venv + # NetCDF C++ legacy API for the build, C base at runtime + sudo apt-get install -y --no-install-recommends \ + libnetcdf-dev libnetcdf-c++4-dev + + - name: Build and install pyforefire + # A venv keeps this clear of PEP 668 externally-managed system Python. + # pip's build isolation pulls scikit-build-core and pybind11 from + # pyproject.toml, and numpy comes in as a runtime dependency. + run: | + python3 -m venv .venv + ./.venv/bin/python -m pip install --upgrade pip + ./.venv/bin/python -m pip install . + + - name: Check pyforefire imports + run: ./.venv/bin/python -c "import pyforefire; print(pyforefire.__file__)" + + - name: Run dead fuel moisture invariants + run: ./.venv/bin/python tests/python/test_moisture_invariants.py diff --git a/README.md b/README.md index bba77256..229a53e3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ [![linuxCI](https://github.com/forefireAPI/forefire/actions/workflows/main.yml/badge.svg)](https://github.com/forefireAPI/forefire/actions/workflows/main.yml) [![macOSCI](https://github.com/forefireAPI/forefire/actions/workflows/macos.yml/badge.svg)](https://github.com/forefireAPI/forefire/actions/workflows/macos.yml) +[![Model Invariants](https://github.com/forefireAPI/forefire/actions/workflows/invariants.yml/badge.svg)](https://github.com/forefireAPI/forefire/actions/workflows/invariants.yml) [![Docker CI/CD](https://github.com/forefireAPI/forefire/actions/workflows/docker.yml/badge.svg)](https://github.com/forefireAPI/forefire/actions/workflows/docker.yml) [![Documentation Status](https://readthedocs.org/projects/forefire/badge/?version=latest)](https://forefire.readthedocs.io/en/latest/?badge=latest) diff --git a/TESTING.md b/TESTING.md index f22ee1da..832a6503 100644 --- a/TESTING.md +++ b/TESTING.md @@ -48,9 +48,44 @@ The `ff-run.bash` script: 3. Uses Python scripts (`compare_kml.py`, `compare_nc.py`) to compare the generated KML and NetCDF files against reference files (`*.ref`) with numerical tolerance, accounting for minor floating-point variations. 4. Exits with status 0 on success, non-zero on failure. +## Running the Model Invariants (`test_moisture_invariants.py`) + +The second test validated in CI, by the `invariants.yml` workflow. Where +`runff` compares ForeFire against frozen ForeFire output — and so cannot tell a +physics fix from a physics regression — this suite holds no reference data. +Every assertion follows from the published spread equations, so it stays valid +across recalibration. + +It asserts, for each propagation model that consumes dead fuel moisture, that +the rate of spread stays finite for any moisture, decreases as moisture rises, +reaches zero at the moisture of extinction, responds to a dynamic +dead-moisture layer, and that `DataBroker` resolves every property the model +registers. + +**To run it manually:** + +1. Install the Python package, which builds the `pyforefire` extension: + ```bash + python3 -m venv .venv + ./.venv/bin/python -m pip install . + ``` +2. Run the suite (add `-v` to print every probe's spread rate): + ```bash + ./.venv/bin/python tests/python/test_moisture_invariants.py + ``` + +Restrict it while iterating with `--model NAME` and `--test NAME`, both +repeatable. It needs no fixtures — fuel, wind, temperature and moisture layers +are built in memory — and takes well under a minute. + +Note that each probe runs in its own interpreter. The C++ core keeps mutable +global state, so a second `ForeFire()` in one process inherits the first one's +parameters and a parameter sweep silently returns one identical result. Keep +that in mind when writing any new Python test that varies parameters. + ## Other Tests -The `tests/` directory contains other subdirectories (`mnh_*`, `python`, `runANN`) for potentially testing specific features like coupled simulations or Python bindings. A main `tests/run.bash` script exists but is not currently fully validated in CI. Refer to specific subdirectories for details if needed. +The `tests/` directory contains other subdirectories (`mnh_*`, `runANN`) for testing specific features like coupled simulations. A main `tests/run.bash` script exists but is not currently fully validated in CI. Refer to specific subdirectories for details if needed. ## Compiler Warnings From 75216d66245d5db7957dd158ce02daaee9a6daa8 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 17:34:49 +0200 Subject: [PATCH 11/63] ci: run the invariants workflow on pull requests to dev Matches the change made to main.yml and macos.yml: the workflow only triggered on master, so nothing integrating into dev would run it. --- .github/workflows/invariants.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index 90760285..abbd5f98 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -13,8 +13,9 @@ on: push: branches: - "master" + - "dev" pull_request: - branches: [ "master" ] + branches: [ "master", "dev" ] workflow_dispatch: jobs: From b65f538ddfcbcf68c4e7a29c3c8b7ed6f624d560 Mon Sep 17 00:00:00 2001 From: Antonio Leblanc Date: Wed, 12 Aug 2026 12:39:24 -0300 Subject: [PATCH 12/63] mv old files --- .../UserGuide/ForeFireExampleCases.tex | 0 .../UserGuide/ForeFireGeneralUsage.tex | 0 .../UserGuide/ForeFireModels.tex | 0 .../UserGuide/ForeFireParams.tex | 0 .../UserGuide/ForeFirePostProcessing.tex | 0 .../UserGuide/ForeFireUserGuide.pdf | Bin .../UserGuide/ForeFireUserGuide.tex | 0 docs/{user_guide_v1 => _legacy/UserGuide}/README.md | 0 .../UserGuide}/images/aullene.JPG | Bin .../UserGuide}/images/fluxComputation.svg | 0 .../UserGuide}/images/forefire.jpg | Bin .../UserGuide}/images/forefire_help.JPG | Bin .../UserGuide}/images/mapOfModels.svg | 0 .../UserGuide}/images/mapOfModelsMap.svg | 0 .../UserGuide}/images/sim-forefire.jpg | Bin 15 files changed, 0 insertions(+), 0 deletions(-) rename docs/{user_guide_v1 => _legacy}/UserGuide/ForeFireExampleCases.tex (100%) rename docs/{user_guide_v1 => _legacy}/UserGuide/ForeFireGeneralUsage.tex (100%) rename docs/{user_guide_v1 => _legacy}/UserGuide/ForeFireModels.tex (100%) rename docs/{user_guide_v1 => _legacy}/UserGuide/ForeFireParams.tex (100%) rename docs/{user_guide_v1 => _legacy}/UserGuide/ForeFirePostProcessing.tex (100%) rename docs/{user_guide_v1 => _legacy}/UserGuide/ForeFireUserGuide.pdf (100%) rename docs/{user_guide_v1 => _legacy}/UserGuide/ForeFireUserGuide.tex (100%) rename docs/{user_guide_v1 => _legacy/UserGuide}/README.md (100%) rename docs/{user_guide_v1 => _legacy/UserGuide}/images/aullene.JPG (100%) rename docs/{user_guide_v1 => _legacy/UserGuide}/images/fluxComputation.svg (100%) rename docs/{user_guide_v1 => _legacy/UserGuide}/images/forefire.jpg (100%) rename docs/{user_guide_v1 => _legacy/UserGuide}/images/forefire_help.JPG (100%) rename docs/{user_guide_v1 => _legacy/UserGuide}/images/mapOfModels.svg (100%) rename docs/{user_guide_v1 => _legacy/UserGuide}/images/mapOfModelsMap.svg (100%) rename docs/{user_guide_v1 => _legacy/UserGuide}/images/sim-forefire.jpg (100%) diff --git a/docs/user_guide_v1/UserGuide/ForeFireExampleCases.tex b/docs/_legacy/UserGuide/ForeFireExampleCases.tex similarity index 100% rename from docs/user_guide_v1/UserGuide/ForeFireExampleCases.tex rename to docs/_legacy/UserGuide/ForeFireExampleCases.tex diff --git a/docs/user_guide_v1/UserGuide/ForeFireGeneralUsage.tex b/docs/_legacy/UserGuide/ForeFireGeneralUsage.tex similarity index 100% rename from docs/user_guide_v1/UserGuide/ForeFireGeneralUsage.tex rename to docs/_legacy/UserGuide/ForeFireGeneralUsage.tex diff --git a/docs/user_guide_v1/UserGuide/ForeFireModels.tex b/docs/_legacy/UserGuide/ForeFireModels.tex similarity index 100% rename from docs/user_guide_v1/UserGuide/ForeFireModels.tex rename to docs/_legacy/UserGuide/ForeFireModels.tex diff --git a/docs/user_guide_v1/UserGuide/ForeFireParams.tex b/docs/_legacy/UserGuide/ForeFireParams.tex similarity index 100% rename from docs/user_guide_v1/UserGuide/ForeFireParams.tex rename to docs/_legacy/UserGuide/ForeFireParams.tex diff --git a/docs/user_guide_v1/UserGuide/ForeFirePostProcessing.tex b/docs/_legacy/UserGuide/ForeFirePostProcessing.tex similarity index 100% rename from docs/user_guide_v1/UserGuide/ForeFirePostProcessing.tex rename to docs/_legacy/UserGuide/ForeFirePostProcessing.tex diff --git a/docs/user_guide_v1/UserGuide/ForeFireUserGuide.pdf b/docs/_legacy/UserGuide/ForeFireUserGuide.pdf similarity index 100% rename from docs/user_guide_v1/UserGuide/ForeFireUserGuide.pdf rename to docs/_legacy/UserGuide/ForeFireUserGuide.pdf diff --git a/docs/user_guide_v1/UserGuide/ForeFireUserGuide.tex b/docs/_legacy/UserGuide/ForeFireUserGuide.tex similarity index 100% rename from docs/user_guide_v1/UserGuide/ForeFireUserGuide.tex rename to docs/_legacy/UserGuide/ForeFireUserGuide.tex diff --git a/docs/user_guide_v1/README.md b/docs/_legacy/UserGuide/README.md similarity index 100% rename from docs/user_guide_v1/README.md rename to docs/_legacy/UserGuide/README.md diff --git a/docs/user_guide_v1/images/aullene.JPG b/docs/_legacy/UserGuide/images/aullene.JPG similarity index 100% rename from docs/user_guide_v1/images/aullene.JPG rename to docs/_legacy/UserGuide/images/aullene.JPG diff --git a/docs/user_guide_v1/images/fluxComputation.svg b/docs/_legacy/UserGuide/images/fluxComputation.svg similarity index 100% rename from docs/user_guide_v1/images/fluxComputation.svg rename to docs/_legacy/UserGuide/images/fluxComputation.svg diff --git a/docs/user_guide_v1/images/forefire.jpg b/docs/_legacy/UserGuide/images/forefire.jpg similarity index 100% rename from docs/user_guide_v1/images/forefire.jpg rename to docs/_legacy/UserGuide/images/forefire.jpg diff --git a/docs/user_guide_v1/images/forefire_help.JPG b/docs/_legacy/UserGuide/images/forefire_help.JPG similarity index 100% rename from docs/user_guide_v1/images/forefire_help.JPG rename to docs/_legacy/UserGuide/images/forefire_help.JPG diff --git a/docs/user_guide_v1/images/mapOfModels.svg b/docs/_legacy/UserGuide/images/mapOfModels.svg similarity index 100% rename from docs/user_guide_v1/images/mapOfModels.svg rename to docs/_legacy/UserGuide/images/mapOfModels.svg diff --git a/docs/user_guide_v1/images/mapOfModelsMap.svg b/docs/_legacy/UserGuide/images/mapOfModelsMap.svg similarity index 100% rename from docs/user_guide_v1/images/mapOfModelsMap.svg rename to docs/_legacy/UserGuide/images/mapOfModelsMap.svg diff --git a/docs/user_guide_v1/images/sim-forefire.jpg b/docs/_legacy/UserGuide/images/sim-forefire.jpg similarity index 100% rename from docs/user_guide_v1/images/sim-forefire.jpg rename to docs/_legacy/UserGuide/images/sim-forefire.jpg From f00d722c8b0accbdd5c43fd3645f23c6673b58e9 Mon Sep 17 00:00:00 2001 From: Antonio Leblanc Date: Wed, 12 Aug 2026 12:39:55 -0300 Subject: [PATCH 13/63] done --- tools/TODO.md | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/TODO.md b/tools/TODO.md index a481d5ce..b4df1bdb 100644 --- a/tools/TODO.md +++ b/tools/TODO.md @@ -1,5 +1,4 @@ Things to do -- Document all code and make the doxygen/readthedocs clean - clean the Python bindings links and pathes, retest - numerical unit test for FF in 2D : circles, area round, ellipses, mass conservation for heat fluxes - numerical tests for fuel models in 2D.. slopes, wind. Best would be 1 model 10 slopes for same wind. Simple ones. From 8b8f5b17141df19a2cacd1f37473ffbbabffe591 Mon Sep 17 00:00:00 2001 From: Antonio Leblanc Date: Wed, 12 Aug 2026 13:44:18 -0300 Subject: [PATCH 14/63] Merge branch 'master' into dev --- tools/devops/install-netcdf-manylinux.sh | 86 +++++++++++++++++++----- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/tools/devops/install-netcdf-manylinux.sh b/tools/devops/install-netcdf-manylinux.sh index f657327b..052aa1ae 100644 --- a/tools/devops/install-netcdf-manylinux.sh +++ b/tools/devops/install-netcdf-manylinux.sh @@ -2,12 +2,21 @@ # # Install the NetCDF stack inside a manylinux_2_28 container, for cibuildwheel. # -# EPEL provides the NetCDF C library and HDF5 for both x86_64 and aarch64, but -# no package for the legacy C++4 API that ForeFire's DataBroker includes as -# , so that one is built from source. Everything lands in /usr/local -# where auditwheel can find it and vendor it into the wheel. +# HDF5 comes from EPEL. The NetCDF C library and the C++4 API are both built +# from source into /usr/local, where auditwheel finds them and vendors them +# into the wheel. +# +# NetCDF C is built rather than installed from EPEL so that DAP and HDF4 can +# be switched off. EPEL enables both, and auditwheel then has to vendor their +# entire dependency closure: DAP pulls libcurl, which drags in OpenSSL, +# Kerberos, LDAP, libssh and a dozen more, about 10 MB across 22 libraries +# that ForeFire never calls. macOS avoids this only because the system +# provides libcurl. HDF4 adds another 1.4 MB, and Homebrew's NetCDF does not +# enable it, so turning it off here also makes the two platforms agree. set -euo pipefail +NETCDF_C_VERSION="${NETCDF_C_VERSION:-4.9.3}" +NETCDF_C_SHA256="a474149844e6144566673facf097fea253dc843c37bc0a7d3de047dc8adda5dd" NETCDF_CXX4_VERSION="${NETCDF_CXX4_VERSION:-4.3.1}" NETCDF_CXX4_SHA256="6a1189a181eed043b5859e15d5c080c30d0e107406fbb212c8fb9814e90f3445" PREFIX="${PREFIX:-/usr/local}" @@ -17,35 +26,82 @@ if [[ -f "${PREFIX}/include/netcdf" ]]; then exit 0 fi -echo "::group::Installing NetCDF C library from EPEL" +echo "::group::Installing HDF5 from EPEL" dnf install -y epel-release -dnf install -y netcdf-devel hdf5-devel +dnf install -y hdf5-devel echo "::endgroup::" -echo "::group::Building netcdf-cxx4 ${NETCDF_CXX4_VERSION}" workdir="$(mktemp -d)" trap 'rm -rf "${workdir}"' EXIT cd "${workdir}" -tarball="netcdf-cxx4-${NETCDF_CXX4_VERSION}.tar.gz" -curl -fsSL -o "${tarball}" \ - "https://downloads.unidata.ucar.edu/netcdf-cxx/${NETCDF_CXX4_VERSION}/${tarball}" -echo "${NETCDF_CXX4_SHA256} ${tarball}" | sha256sum --check --strict +fetch() { + local url="$1" tarball="$2" sha="$3" + curl -fsSL -o "${tarball}" "${url}" + echo "${sha} ${tarball}" | sha256sum --check --strict + tar xzf "${tarball}" +} + +echo "::group::Building netcdf-c ${NETCDF_C_VERSION}" +fetch "https://downloads.unidata.ucar.edu/netcdf-c/${NETCDF_C_VERSION}/netcdf-c-${NETCDF_C_VERSION}.tar.gz" \ + "netcdf-c-${NETCDF_C_VERSION}.tar.gz" "${NETCDF_C_SHA256}" +cd "netcdf-c-${NETCDF_C_VERSION}" -tar xzf "${tarball}" +# --disable-dap and --disable-byterange both have to go: byterange is a second +# remote-access path that links libcurl on its own. --disable-hdf4 drops +# libmfhdf, libdf, libjpeg and libtirpc. ForeFire reads local .nc and PGD +# files, so none of these are reachable from it. +./configure \ + --prefix="${PREFIX}" \ + --enable-shared \ + --disable-static \ + --disable-dap \ + --disable-byterange \ + --disable-hdf4 \ + --disable-libxml2 \ + --disable-examples \ + --disable-testsets +make -j "$(nproc)" +make install +ldconfig +cd "${workdir}" +echo "::endgroup::" + +echo "::group::Building netcdf-cxx4 ${NETCDF_CXX4_VERSION}" +fetch "https://downloads.unidata.ucar.edu/netcdf-cxx/${NETCDF_CXX4_VERSION}/netcdf-cxx4-${NETCDF_CXX4_VERSION}.tar.gz" \ + "netcdf-cxx4-${NETCDF_CXX4_VERSION}.tar.gz" "${NETCDF_CXX4_SHA256}" cd "netcdf-cxx4-${NETCDF_CXX4_VERSION}" +# nc-config from the netcdf-c just installed has to win over anything else on +# PATH, so that the C++ API links the DAP-free build. +export PATH="${PREFIX}/bin:${PATH}" +export PKG_CONFIG_PATH="${PREFIX}/lib:${PREFIX}/lib64:${PKG_CONFIG_PATH:-}" + # Autotools rather than the bundled CMake build: netcdf-cxx4 4.3.1 declares # `cmake_minimum_required(VERSION 2.8.12)`, and CMake 4 refuses anything below -# 3.5. configure picks up the EPEL NetCDF through nc-config. +# 3.5. configure picks up the NetCDF built above through nc-config. ./configure \ --prefix="${PREFIX}" \ --enable-shared \ --disable-static -make -j "$(nproc)" -make install + +# SUBDIRS is overridden to skip examples/. They call nc_set_log_level, which +# netcdf-c only exports when built --enable-logging, so they fail to link +# against our build. 4.3.1 has no --disable-examples. The library, all the +# headers including the umbrella, and the pkg-config file are still +# installed. +make -j "$(nproc)" SUBDIRS=cxx4 +make install SUBDIRS=cxx4 ldconfig echo "::endgroup::" echo "NetCDF C++4 installed:" ls -l "${PREFIX}"/lib*/libnetcdf_c++4.so* "${PREFIX}/include/netcdf" + +# Fail loudly here rather than shipping a wheel that quietly regained the +# dependency this script exists to avoid. +if ldd "${PREFIX}"/lib*/libnetcdf.so | grep -q libcurl; then + echo "ERROR: libnetcdf still links libcurl; DAP or byterange is enabled." >&2 + exit 1 +fi +echo "Confirmed: libnetcdf does not link libcurl." From 7c769c74a4f5335fe1e524bf888458c91523c7f2 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 19:38:44 +0200 Subject: [PATCH 15/63] test(http): pin the HTTP command server's behaviour before hardening it Nothing tested the HTTP server, and it is due for changes to request parsing and path resolution. These seven cases record what it does today, so a regression shows up here rather than as a blank map in a browser. Two behaviours the web console depends on and that any hardening must preserve: - POST / with a body of "ff:" reaches the interpreter. That is the entire UI-to-engine channel (forefireGUI.js sendCommand). - Files resolve against the working directory. The console issues plot[filename=fuel.png], ForeFire writes fuel.png beside the case, and the browser fetches /fuel.png as a static file. A document root that does not cover both htdocs and the working directory would break every overlay. Deliberately not covered: path traversal. Asserting the current behaviour would document an unfixed weakness in a public repository. Its test belongs with the fix, when the assertion can be that the request is refused. Two problems found while writing this, neither fixed here: - HttpCommandServer.hpp was not self-contained. It used std::ifstream without including and an unqualified cout, so it compiled only when included after a header that supplied both. Fixed, because the test cannot include it otherwise; no behaviour change. - stop() deadlocks. It closes the listening socket and then joins the accept thread, but closing a socket does not reliably wake a thread blocked in accept() on Linux, so the join never returns. The suite therefore starts one server and never stops it. Fixing that needs shutdown() or a self-pipe and belongs with the hardening work. --- src/HttpCommandServer.hpp | 3 +- tests/unit/CMakeLists.txt | 3 +- tests/unit/test_http_server.cpp | 246 ++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_http_server.cpp diff --git a/src/HttpCommandServer.hpp b/src/HttpCommandServer.hpp index 5708462a..ba80f7bb 100644 --- a/src/HttpCommandServer.hpp +++ b/src/HttpCommandServer.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -298,7 +299,7 @@ namespace http_command { path = altPath; } } else { - cout << "FOREFIREHOME not set. File not found: " << path << std::endl; + std::cout << "FOREFIREHOME not set. File not found: " << path << std::endl; } } if (fileExists(path)) { diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 96136ce0..18ecabd9 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable(forefire_unit_tests main.cpp model_sandbox.cpp test_flux_models.cpp + test_http_server.cpp test_model_registry.cpp test_propagation_models.cpp) @@ -24,7 +25,7 @@ endif() # doctest groups its cases into suites; registering one CTest entry per suite # keeps `ctest` output useful without needing doctest's CMake integration. -foreach(_suite "model registry" "propagation models" "flux models") +foreach(_suite "model registry" "propagation models" "flux models" "http server") string(REPLACE " " "_" _suite_id "${_suite}") add_test(NAME "unit.${_suite_id}" COMMAND forefire_unit_tests --test-suite=${_suite} --no-skipped-summary) diff --git a/tests/unit/test_http_server.cpp b/tests/unit/test_http_server.cpp new file mode 100644 index 00000000..ebd24883 --- /dev/null +++ b/tests/unit/test_http_server.cpp @@ -0,0 +1,246 @@ +/** + * @file test_http_server.cpp + * @brief What the HTTP command server does today, pinned before it is changed. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + * + * The server is due for hardening, and nothing tested it. These cases record + * the behaviour the web console in tools/htdocs depends on, so that a change + * to request parsing or path resolution shows up here rather than as a blank + * map in someone's browser. + * + * Two things the console needs, both covered below: + * + * - POST / with a body of "ff:" reaches the interpreter. That is + * the whole UI-to-engine channel (forefireGUI.js sendCommand). + * - Files are served relative to the working directory. The console issues + * plot[filename=fuel.png], ForeFire writes fuel.png next to the case, and + * the browser then fetches /fuel.png as a static file. Confining the + * document root without accounting for this would break every overlay. + * + * These are characterisation tests: they say what happens now, not what ought + * to. Where current behaviour is a known limitation it is marked, so that a + * case flipping is read as "the fix landed" rather than "something broke". + */ + +#include "doctest/doctest.h" + +#include "HttpCommandServer.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +using http_command::HttpCommandServer; + +namespace { + +/** Sends one raw request and reads the whole response. + * + * The server closes the connection after replying, so reading to EOF is + * enough — no need to parse Content-Length on this side. */ +std::string request(int port, const std::string& raw) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + REQUIRE(fd >= 0); + + sockaddr_in addr; + std::memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(static_cast(port)); + REQUIRE(inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) == 1); + + if (connect(fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + close(fd); + return ""; + } + if (write(fd, raw.data(), raw.size()) < 0) { + close(fd); + return ""; + } + + std::string response; + char buffer[4096]; + ssize_t n; + while ((n = read(fd, buffer, sizeof(buffer))) > 0) { + response.append(buffer, static_cast(n)); + } + close(fd); + return response; +} + +/** What the callback was last handed, so a test can check the "ff:" prefix is + * stripped rather than only that a reply came back. */ +std::string& lastCommand() { + static std::string command; + return command; +} + +/** One server, started once and deliberately never stopped. + * + * stop() cannot be called: it sets running=false, closes the listening socket + * and then joins the accept thread — but closing a socket does not reliably + * wake a thread blocked in accept() on Linux, so the join never returns and + * the process hangs. Fixing that needs shutdown() or a self-pipe, which is a + * change to server behaviour and belongs with the pending hardening work + * rather than with a test that is supposed to be purely additive. + * + * Leaking it is safe here: the object is never destroyed, so the joinable + * std::thread member never runs its destructor, and the thread dies with the + * process. Sharing one server across the suite also keeps the port scan to a + * single pass. + * + * The address is given as 127.0.0.1 deliberately. The host half is currently + * discarded by listenOn, which binds INADDR_ANY regardless — writing it this + * way means these tests keep passing once the address is honoured. */ +int serverPort() { + static int port = [] { + HttpCommandServer* server = new HttpCommandServer(); // never deleted + server->setCallback([](const std::string& command) { + lastCommand() = command; + return "callback saw: " + command; + }); + // Nothing reports the bound port back, so try until one takes. + for (int candidate = 18730; candidate < 18790; candidate++) { + char address[32]; + std::snprintf(address, sizeof(address), "127.0.0.1:%d", candidate); + if (server->listenOn(address)) return candidate; + } + return 0; + }(); + return port; +} + +/** Writes a file into the working directory and removes it afterwards. + * + * The working directory is where the server looks, and where ForeFire writes + * the PNGs the console then requests. */ +class ScratchFile { +public: + ScratchFile(const std::string& name, const std::string& contents) + : path(name) { + std::ofstream out(path.c_str(), std::ios::binary); + out << contents; + } + ~ScratchFile() { std::remove(path.c_str()); } + +private: + ScratchFile(const ScratchFile&); + ScratchFile& operator=(const ScratchFile&); + std::string path; +}; + +bool contains(const std::string& haystack, const std::string& needle) { + return haystack.find(needle) != std::string::npos; +} + +} /* anonymous namespace */ + +TEST_SUITE("http server") { + +TEST_CASE("the server binds and accepts a connection") { + REQUIRE_MESSAGE(serverPort() != 0, + "no free port in the test range; nothing else is testable"); + + const std::string response = + request(serverPort(), "GET /nothing-here HTTP/1.1\r\n\r\n"); + CHECK_FALSE(response.empty()); +} + +TEST_CASE("a file in the working directory is served") { + // This is the path the web console relies on for plot overlays: ForeFire + // writes .png beside the case, the browser fetches /.png. + REQUIRE(serverPort() != 0); + lastCommand().clear(); + ScratchFile file("ff_test_overlay.txt", "overlay-body"); + + const std::string response = + request(serverPort(), "GET /ff_test_overlay.txt HTTP/1.1\r\n\r\n"); + + CHECK(contains(response, "200 OK")); + CHECK(contains(response, "overlay-body")); +} + +TEST_CASE("a missing file returns 404") { + REQUIRE(serverPort() != 0); + lastCommand().clear(); + + const std::string response = + request(serverPort(), "GET /definitely_absent_file HTTP/1.1\r\n\r\n"); + + CHECK(contains(response, "404 Not Found")); +} + +TEST_CASE("a POST body prefixed with ff: reaches the command callback") { + // forefireGUI.js sendCommand() posts to "/" with body "ff:". + // This is the entire UI-to-engine channel. + REQUIRE(serverPort() != 0); + lastCommand().clear(); + + const std::string body = "ff:getParameter[ISOdate]"; + std::string raw = "POST / HTTP/1.1\r\n"; + raw += "Content-Type: text/plain\r\n"; + raw += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n"; + raw += body; + + const std::string response = request(serverPort(), raw); + + CHECK(contains(response, "200 OK")); + CHECK(contains(response, "callback saw: getParameter[ISOdate]")); + // The "ff:" prefix must be stripped before the interpreter sees it. + CHECK(lastCommand() == "getParameter[ISOdate]"); +} + +TEST_CASE("a GET uri prefixed with /ff: reaches the command callback") { + REQUIRE(serverPort() != 0); + lastCommand().clear(); + + const std::string response = + request(serverPort(), "GET /ff:print[] HTTP/1.1\r\n\r\n"); + + CHECK(contains(response, "200 OK")); + CHECK(contains(response, "callback saw: print[]")); + CHECK(lastCommand() == "print[]"); +} + +TEST_CASE("an ordinary file request does not reach the command callback") { + // Static assets and commands share one endpoint, so the split between + // them is worth pinning: js/style/png requests must not be interpreted. + REQUIRE(serverPort() != 0); + lastCommand().clear(); + ScratchFile file("ff_test_asset.css", "body{}"); + + const std::string response = + request(serverPort(), "GET /ff_test_asset.css HTTP/1.1\r\n\r\n"); + + CHECK(contains(response, "200 OK")); + CHECK(lastCommand().empty()); +} + +TEST_CASE("a request comfortably inside one buffer round-trips intact") { + // handleClient currently reads a single fixed-size buffer rather than + // consulting Content-Length, so only requests well under that size are + // guaranteed. This pins a size the console can rely on today; raising it + // is part of the pending hardening work. + REQUIRE(serverPort() != 0); + lastCommand().clear(); + + const std::string argument(1000, 'x'); + const std::string body = "ff:setParameter[note=" + argument + "]"; + std::string raw = "POST / HTTP/1.1\r\n"; + raw += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n"; + raw += body; + + const std::string response = request(serverPort(), raw); + + CHECK(contains(response, "200 OK")); + CHECK(contains(lastCommand(), argument)); + CHECK(lastCommand().size() == body.size() - 3); +} + +} /* TEST_SUITE */ From eeead4fa142f480a1f87b92a1842d297c619f402 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Tue, 11 Aug 2026 16:57:51 +0200 Subject: [PATCH 16/63] test: add a concurrency stress test for free-threaded builds Eight simulations in threads, each required to reproduce the fire node count it produces when run alone, plus a case hammering object construction to exercise the shared id counter. Skips when the GIL is on, since every call into the extension is serialised there and no race can show. Under PYTHON_GIL=0 on a free-threaded interpreter it segfaults or returns wrong counts, which is the point: it gives the shared-state work in #175 a failing test to fix. Deliberately not wired into CI. On an ordinary interpreter it would skip and report green while proving nothing; it earns a job once it can pass for the right reason. Refs #175 --- TESTING.md | 28 +++++++ tests/python/test_threading.py | 149 +++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 tests/python/test_threading.py diff --git a/TESTING.md b/TESTING.md index 832a6503..c42ea85c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -83,6 +83,34 @@ global state, so a second `ForeFire()` in one process inherits the first one's parameters and a parameter sweep silently returns one identical result. Keep that in mind when writing any new Python test that varies parameters. +## Running the Concurrency Stress Test (`test_threading.py`) + +Not run in CI, deliberately. On an ordinary interpreter the GIL serialises +every call into the extension, so the test skips and would report a green +result that proves nothing. It is a reproduction tool, run by hand when +working on the shared-state problem in +[#175](https://github.com/forefireAPI/forefire/issues/175). + +It runs eight simulations in threads and requires each to reproduce the fire +node count it produces when run alone, plus a case that hammers object +construction to exercise the shared id counter. + +**To run it**, you need a free-threaded build of CPython (`python3.14t` or +later) and `PYTHON_GIL=0`. The variable is required: `_pyforefire` does not +declare `py::mod_gil_not_used()`, so importing it switches the GIL back on and +the test skips. + +```bash +python3.14t -m venv .venv +./.venv/bin/python -m pip install . +PYTHON_GIL=0 ./.venv/bin/python tests/python/test_threading.py +``` + +**It does not pass today.** On `dev` it segfaults or returns wrong node counts, +which is the point — it is the failing test the work in #175 has to make pass. +Wiring it into CI belongs with the last step of that issue, once it can pass +for the right reason. + ## Other Tests The `tests/` directory contains other subdirectories (`mnh_*`, `runANN`) for testing specific features like coupled simulations. A main `tests/run.bash` script exists but is not currently fully validated in CI. Refer to specific subdirectories for details if needed. diff --git a/tests/python/test_threading.py b/tests/python/test_threading.py new file mode 100644 index 00000000..ed073194 --- /dev/null +++ b/tests/python/test_threading.py @@ -0,0 +1,149 @@ +"""Concurrency stress test for running several simulations in one process. + +ForeFire keeps simulation state in process-wide statics, so two domains +running at the same time can corrupt each other. Under a normal CPython the +GIL serialises every call into the extension and hides all of it, which is why +this test only means anything on a free-threaded interpreter with the GIL +actually off: + + PYTHON_GIL=0 python3.14t tests/python/test_threading.py + +`PYTHON_GIL=0` is needed because `_pyforefire` does not declare +`py::mod_gil_not_used()`, so importing it would otherwise switch the GIL back +on and the test would pass without proving anything. + +Exit status is 0 only if every thread produced exactly the result it produces +when run alone. +""" + +import os +import sys +import threading +import traceback + +SIZE = 1000 +STEPS = 5 +THREADS = 8 + + +def gil_enabled(): + """True if the GIL is active, None if this interpreter has no switch.""" + if not hasattr(sys, "_is_gil_enabled"): + return None + return sys._is_gil_enabled() + + +def run_simulation(seed): + """One self-contained simulation. Returns its fire node count.""" + import pyforefire + + ff = pyforefire.ForeFire() + ff.execute(f"FireDomain[sw=(0,0,0);ne=({SIZE},{SIZE},0);t=0]") + ff.addLayer("propagation", "Iso", "propagationModel") + ff.execute(f"startFire[loc=({SIZE / 2},{SIZE / 2},0)]") + for _ in range(STEPS): + ff.execute("step[dt=100]") + return ff.execute("print[]").count("FireNode") + + +def test_concurrent_domains(baseline): + """Every thread must reproduce the single-threaded result exactly.""" + results = [None] * THREADS + errors = [None] * THREADS + + def worker(i): + try: + results[i] = run_simulation(i) + except BaseException: + errors[i] = traceback.format_exc() + + threads = [threading.Thread(target=worker, args=(i,)) for i in range(THREADS)] + for t in threads: + t.start() + for t in threads: + t.join() + + failures = [] + for i, err in enumerate(errors): + if err is not None: + failures.append(f"thread {i} raised:\n{err}") + for i, got in enumerate(results): + if errors[i] is None and got != baseline: + failures.append(f"thread {i} produced {got} fire nodes, expected {baseline}") + return failures + + +def test_concurrent_construction(): + """Hammer object creation, which draws IDs from a shared counter. + + Every ForeFireAtom takes its id from `instanceNRCount++`, which is not + atomic, so concurrent construction can hand the same id to two objects. + """ + import pyforefire + + made = [] + lock = threading.Lock() + errors = [] + + def worker(): + try: + local = [pyforefire.ForeFire() for _ in range(25)] + with lock: + made.extend(local) + except BaseException: + with lock: + errors.append(traceback.format_exc()) + + threads = [threading.Thread(target=worker) for _ in range(THREADS)] + for t in threads: + t.start() + for t in threads: + t.join() + + if errors: + return [f"construction raised:\n{errors[0]}"] + expected = THREADS * 25 + if len(made) != expected: + return [f"created {len(made)} objects, expected {expected}"] + return [] + + +def main(): + # Import before reading the GIL state. An extension that does not declare + # `py::mod_gil_not_used()` switches the GIL back on as it is imported, so + # checking beforehand reports the interpreter's default rather than the + # state this test actually runs under. + before = gil_enabled() + import pyforefire # noqa: F401 + + gil = gil_enabled() + print(f"interpreter: {sys.version.split()[0]}") + print(f"GIL enabled: {gil} (was {before} before importing pyforefire)") + if gil is None: + print("SKIP: not a free-threaded interpreter, this test cannot prove anything") + return 0 + if gil: + print( + "SKIP: the GIL is on, so every call is serialised and races stay hidden.\n" + " Re-run with PYTHON_GIL=0 to actually exercise concurrency." + ) + return 0 + + baseline = run_simulation(0) + print(f"single-threaded baseline: {baseline} fire nodes") + + failures = [] + failures += test_concurrent_domains(baseline) + failures += test_concurrent_construction() + + if failures: + print(f"\nFAILED with {len(failures)} problem(s):") + for f in failures[:10]: + print(f" - {f}") + return 1 + print(f"\nOK: {THREADS} concurrent simulations all matched the baseline") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From f95a5b25bb4646b875ebbe671ceab68be7eb6af9 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Tue, 11 Aug 2026 17:11:34 +0200 Subject: [PATCH 17/63] fix(core): make the atom id counter atomic and the parameters singleton safe Two races that the GIL currently hides. ForeFireAtom::instanceNRCount was a plain long incremented with ++ to give every atom its id. Concurrent construction could hand the same id to two objects. It is now std::atomic with a relaxed fetch_add, which is all the ordering distinct ids need. SimulationParameters::GetInstance used the textbook unsafe lazy singleton, so two threads could both see null and both construct, then diverge with different parameter objects. It is now a function-local static, whose initialisation C++11 guarantees runs once. The unused private member is gone. --- src/FireDomain.cpp | 2 +- src/ForeFireAtom.h | 15 +++++++++++++-- src/SimulationParameters.cpp | 10 +++++++--- src/SimulationParameters.h | 2 -- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/FireDomain.cpp b/src/FireDomain.cpp index 240c5af2..269afcd1 100644 --- a/src/FireDomain.cpp +++ b/src/FireDomain.cpp @@ -27,7 +27,7 @@ // On fait plus de chaines.... // Static variables - long ForeFireAtom::instanceNRCount = 0; + std::atomic ForeFireAtom::instanceNRCount{0}; const double FireDomain::endChain = -1.; const double FireDomain::endCom = -10.; diff --git a/src/ForeFireAtom.h b/src/ForeFireAtom.h index f8b56ca0..378df6f8 100644 --- a/src/ForeFireAtom.h +++ b/src/ForeFireAtom.h @@ -11,6 +11,8 @@ #include "include/Futils.h" +#include + using namespace std; namespace libforefire{ @@ -33,7 +35,13 @@ namespace libforefire{ class ForeFireAtom { private: - static long instanceNRCount; /*!< Instance Count */ + /*! \brief Instance count, the source of every atom's id. + * + * Atomic because objects are created from more than one thread once the + * GIL is out of the way, and a plain `++` there is a data race that can + * hand the same id to two atoms. + */ + static std::atomic instanceNRCount; double time; /*!< current time of the object */ double updateTime; /*!< next update time */ @@ -103,7 +111,10 @@ class ForeFireAtom { return getIDfromLongs((long) did/domainMult(),(long) did%domainMult()); } void getNewID(const long& domainId){ - numID = getIDfromLongs(domainId,instanceNRCount++); + // relaxed: ids only have to be distinct, not ordered against other + // memory operations. + numID = getIDfromLongs(domainId, + instanceNRCount.fetch_add(1, std::memory_order_relaxed)); } /*! \brief Pure virtual function for inpus */ diff --git a/src/SimulationParameters.cpp b/src/SimulationParameters.cpp index d01a3be9..024e4322 100644 --- a/src/SimulationParameters.cpp +++ b/src/SimulationParameters.cpp @@ -17,8 +17,6 @@ namespace libforefire { -SimulationParameters* SimulationParameters::instance = 0; - string SimulationParameters::undefined = "1234567890"; double SimulationParameters::doubleUndefined = 1234567890.; int SimulationParameters::intUndefined = 1234567890; @@ -26,7 +24,13 @@ size_t SimulationParameters::sizeUndefined = 1234567890; SimulationParameters* SimulationParameters::GetInstance(){ - if ( instance == 0 ) instance = new SimulationParameters; + // A function-local static: C++11 onwards guarantees its initialisation + // runs exactly once even if several threads arrive here together. The + // previous `if (instance == 0) instance = new ...` let two threads both + // see null and both construct, leaving them with different parameter + // objects. Never deleted, matching the previous behaviour; the parameters + // live for the whole process. + static SimulationParameters* const instance = new SimulationParameters(); return instance; } diff --git a/src/SimulationParameters.h b/src/SimulationParameters.h index 7be1cb4b..06551d71 100644 --- a/src/SimulationParameters.h +++ b/src/SimulationParameters.h @@ -18,8 +18,6 @@ namespace libforefire { class SimulationParameters { - static SimulationParameters* instance; /*!< Singleton-type class */ - /*! values for undefined parameters */ static string undefined; static double doubleUndefined; From b3ff1ffbdddf3272532e9cb0872b7bdf8b70cea8 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 20:28:19 +0200 Subject: [PATCH 18/63] refactor(core): give each string representation its own buffer StringRepresentation kept the output buffer, the nesting level and the GeoJSON ring aggregation as statics and file globals, so two representations in one process built into the same state. All of it is per-instance now. The file-scope 'outPattern' and 'domain' globals were shadowed by members of the same name in every member function, so they were dead; removed with the rest. writedOnce stays a file global on purpose. Making it per-instance is the one part of this that is observable: output() skips a representation whose domain has no front once anything has been written, so the second representation a coupled session creates would gain a first output file it does not produce today. That is a behaviour decision for the coupled runs rather than a mechanical one, and it belongs with the state work. outputstr stays public because Command::dumpString reads it directly. rungeojson.ff produces byte-identical output to dev, both files (md5 5598d4b548bd7826546815f822b01073 and 177683c7bb42b267be6baa1d98ef5acd). Refs #175 --- src/StringRepresentation.cpp | 26 ++++++++++++++------------ src/StringRepresentation.h | 13 +++++++++++-- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/StringRepresentation.cpp b/src/StringRepresentation.cpp index 89def449..f3bf6f29 100644 --- a/src/StringRepresentation.cpp +++ b/src/StringRepresentation.cpp @@ -24,19 +24,21 @@ using namespace std; namespace libforefire { -// static variables initialization -size_t StringRepresentation::currentLevel = 0; -ostringstream StringRepresentation::outputstr(""); +// The output buffer, the nesting level and the GeoJSON aggregation state used +// to be statics and file globals here, so two simulations printing at the same +// time wrote into one buffer. They are members now. +// +// For GEOJSON mode we aggregate rings per top-level front. Each top-level +// feature (currentLevel==1) is a vector of rings, where each ring is a vector +// of coordinate strings. +// +// writedOnce is deliberately left shared for now. Unlike the rest, making it +// per-instance is an observable change: output() skips a representation whose +// domain has no front once anything has been written, so a second +// representation in the same session would gain a first output file it does +// not produce today. That is a behaviour question for the coupled runs, not a +// mechanical one, and it belongs with the state work in #175. bool writedOnce = false; -string outPattern; -FireDomain* domain = 0; - -// --- GEOJSON Aggregation Variables --- -// For GEOJSON mode we aggregate rings per top-level front. -// Each top-level feature (currentLevel==1) will be represented as a vector of rings, -// where each ring is a vector of coordinate strings. -static bool firstGeoFeature = true; // used when outputting the feature list -static std::vector< std::vector > geojson_current_feature; StringRepresentation::StringRepresentation(FireDomain* fdom) : Visitor() { lastLevel = -1; diff --git a/src/StringRepresentation.h b/src/StringRepresentation.h index 761f5c42..8fbad990 100644 --- a/src/StringRepresentation.h +++ b/src/StringRepresentation.h @@ -18,13 +18,22 @@ namespace libforefire { class StringRepresentation: public Visitor { FireDomain* domain; - static size_t currentLevel; + + /* These were statics, shared by every simulation in the process: two + * concurrent print[] calls interleaved into one buffer. */ + size_t currentLevel = 0; + bool firstGeoFeature = true; /*!< first feature of the GeoJSON list */ + std::vector< std::vector > geojson_current_feature; double updateStep; public: - static ostringstream outputstr; + /*! \brief buffer the representation is built into + * + * Was static, so two simulations printing at once interleaved into one + * buffer. Command::dumpString reads it, hence public. */ + ostringstream outputstr; /* StringRepresentation();*/ StringRepresentation(FireDomain*); From 41af42e2dc1aedab2676b6db7647950bc2b4f213 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 21:10:38 +0200 Subject: [PATCH 19/63] fix(tools): restore genForeFireCase.py, with the axis order fixed The script that writes the NetCDF landscape file was deleted in ac0baba while four documents kept describing it, so anyone following the docs to build their first case hit a file that was not there. Restored from ac0baba^ with three fixes, since it was not usable as it stood: - The 3-D and 4-D paths were broken. Dimensions were read off the array as (NY, NX, NZ, NT) while the variable was created as (NT, NZ, NY, NX) and assigned untransposed, so a 4-D field raised a broadcast error and a 3-D one never had its NT dimension created. Fields are now indexed outermost axis first, which is what prealCF2Case.py in the same directory settled on when it fixed its own copy. - scipy.io.netcdf is deprecated and scipy is not a project dependency. Uses netCDF4, which the tests already require, writing the same NETCDF3_CLASSIC format prealCF2Case.py writes. - parametersProperties has seven required keys and the docs called them optional. They are checked before the file is opened, and named in the error, rather than raising a bare KeyError over a half-written file. tests/python/test_genforefirecase.py builds a landscape, loads it in ForeFire, ignites and steps, and pins the 3-D and 4-D shapes. The four documents now describe what exists. prealCF2Case.py still carries its own copy of both functions. Merging them is a change to a working script and is left for its own commit. Closes #167 --- TESTING.md | 20 +++ docs/source/user_guide/landscape_file.rst | 21 ++- tests/python/test_genforefirecase.py | 208 ++++++++++++++++++++++ tools/README.md | 17 +- tools/preprocessing/READMEscripts.md | 4 +- tools/preprocessing/genForeFireCase.py | 164 +++++++++++++++++ 6 files changed, 427 insertions(+), 7 deletions(-) create mode 100644 tests/python/test_genforefirecase.py create mode 100644 tools/preprocessing/genForeFireCase.py diff --git a/TESTING.md b/TESTING.md index c42ea85c..e9a23843 100644 --- a/TESTING.md +++ b/TESTING.md @@ -111,6 +111,26 @@ which is the point — it is the failing test the work in #175 has to make pass. Wiring it into CI belongs with the last step of that issue, once it can pass for the right reason. +## Running the Landscape Generator Test (`test_genforefirecase.py`) + +Covers `tools/preprocessing/genForeFireCase.py`, which writes the NetCDF +landscape file a simulation runs on. It builds a landscape, hands it to +ForeFire, ignites it and steps, so it exercises the writer and the reader +together rather than the writer alone. It also pins the field axis order, +where the 3-D and 4-D paths were previously broken. + +**To run it manually**, after installing the Python package as above: + +```bash +./.venv/bin/python tests/python/test_genforefirecase.py +``` + +It needs `numpy` and `netCDF4`, writes only into a temporary directory, and +takes a few seconds. Without a built `pyforefire` the load-and-simulate case +skips and the rest still run, so it is usable while iterating on the writer. + +The test doubles as the worked example for the tool. + ## Other Tests The `tests/` directory contains other subdirectories (`mnh_*`, `runANN`) for testing specific features like coupled simulations. A main `tests/run.bash` script exists but is not currently fully validated in CI. Refer to specific subdirectories for details if needed. diff --git a/docs/source/user_guide/landscape_file.rst b/docs/source/user_guide/landscape_file.rst index e45ed44e..cd11d8f5 100644 --- a/docs/source/user_guide/landscape_file.rst +++ b/docs/source/user_guide/landscape_file.rst @@ -81,7 +81,26 @@ Generating a suitable ``landscape.nc`` file typically involves standard Geograph * **Clip** the layers to your desired simulation domain boundaries. * **Convert** the final processed layers into a single NetCDF file with appropriate variable names. -3. **ForeFire Helpers (Potentially):** Previous versions of ForeFire included Python helper scripts (e.g., `genForeFireCase.py`). While the status of V2 helpers is pending, tools might exist or be developed to assist in this NetCDF creation process. Consult the documentation sections on available tools once updated. +3. **ForeFire Helpers:** ``tools/preprocessing/genForeFireCase.py`` writes the file for you from numpy arrays, which is usually easier than assembling the NetCDF by hand: + + .. code-block:: python + + import numpy as np + from genForeFireCase import FiretoNC + + FiretoNC("landscape.nc", + domainProperties={'SWx': 0., 'SWy': 0., 'SWz': 0., + 'Lx': 1000., 'Ly': 1000., 'Lz': 0., + 't0': 0., 'Lt': np.inf}, + parametersProperties={'date': "2026-08-12T12:00:00Z", + 'duration': 3600, + 'refYear': 2026, 'refDay': 224, + 'year': 2026, 'month': 8, 'day': 12}, + fuelModelMap=fuel, # (NY, NX) integer fuel indices + elevation=elevation, # (NY, NX) metres + wind={"zonal": windU, "meridian": windV}) + + Field arrays are indexed outermost axis first: ``(NY, NX)``, or ``(NZ, NY, NX)`` and ``(NT, NZ, NY, NX)`` when they vary with height or time. Every key shown under ``parametersProperties`` is required. Loading in ForeFire ------------------- diff --git a/tests/python/test_genforefirecase.py b/tests/python/test_genforefirecase.py new file mode 100644 index 00000000..83697879 --- /dev/null +++ b/tests/python/test_genforefirecase.py @@ -0,0 +1,208 @@ +"""Smoke test for tools/preprocessing/genForeFireCase.py. + +The landscape file is the thing standing between a new user and their first +simulation, so this checks the whole path rather than the writer alone: build +a landscape, hand it to ForeFire, ignite it, and step. + +It also pins the axis order. The 3-D and 4-D paths used to declare their +dimensions in one order and assign in another: a 4-D field only broadcast when +NY == NT and NX == NZ, and a 3-D field never got its NT dimension created. + + python3 tests/python/test_genforefirecase.py + +Needs numpy, netCDF4 and a built pyforefire. Skips rather than fails when +pyforefire is missing, so it stays runnable on a machine with no build. +""" + +import os +import sys +import tempfile +import traceback + +sys.path.insert(0, os.path.join( + os.path.dirname(os.path.abspath(__file__)), "..", "..", + "tools", "preprocessing")) + +import numpy as np # noqa: E402 +from netCDF4 import Dataset # noqa: E402 + +from genForeFireCase import FiretoNC, REQUIRED_PARAMETERS # noqa: E402 + +NX = 100 +NY = 100 + + +def domain_properties(nx=NX, ny=NY, resolution=10.0): + return {'SWx': 0., 'SWy': 0., 'SWz': 0., + 'Lx': nx * resolution, 'Ly': ny * resolution, 'Lz': 0., + 't0': 0., 'Lt': np.inf} + + +def parameters_properties(): + return {'date': "2026-08-12T12:00:00Z", 'duration': 3600, + 'refYear': 2026, 'refDay': 224, + 'year': 2026, 'month': 8, 'day': 12} + + +def test_writes_a_loadable_landscape(): + """The generated file loads in ForeFire, ignites and spreads.""" + try: + import pyforefire + except ImportError: + return ["SKIP: pyforefire not importable"] + + failures = [] + with tempfile.TemporaryDirectory() as workdir: + path = os.path.join(workdir, "landscape.nc") + + fuel = np.full((NY, NX), 1, dtype=np.int32) + elevation = np.zeros((NY, NX)) + wind = {"zonal": np.full((NY, NX), 2.0), + "meridian": np.zeros((NY, NX))} + + FiretoNC(path, domain_properties(), parameters_properties(), + fuel, elevation=elevation, wind=wind) + + with Dataset(path) as ds: + for name in ('fuel', 'altitude', 'windU', 'windV', + 'domain', 'parameters'): + if name not in ds.variables: + failures.append("landscape is missing variable %s" % name) + if failures: + return failures + + ff = pyforefire.ForeFire() + ff.setString("fuelsTableFile", + os.path.join(os.path.dirname(os.path.abspath(__file__)), + "..", "runff", "fuels.csv")) + ff.setString("NetCDFfile", path) + ff.execute("FireDomain[sw=(0,0,0);ne=(1000,1000,0);t=0]") + ff.addLayer("propagation", "Rothermel", "propagationModel") + ff.execute("startFire[loc=(500,500,0);t=0]") + for _ in range(5): + ff.execute("step[dt=100]") + + nodes = ff.execute("print[]").count("FireNode") + if nodes == 0: + failures.append("no fire nodes after stepping a loaded landscape") + + return failures + + +def test_four_dimensional_field(): + """A 4-D field keeps its shape and its values. + + Deliberately uses four different lengths: with NY == NT and NX == NZ the + old code's broadcast succeeded by accident. + """ + failures = [] + nt, nz, ny, nx = 5, 2, 3, 4 + + with tempfile.TemporaryDirectory() as workdir: + path = os.path.join(workdir, "landscape.nc") + + fuel = np.full((ny, nx), 1, dtype=np.int32) + # Every element distinct, so a reordering cannot go unnoticed. + zonal = np.arange(nt * nz * ny * nx, dtype=float).reshape( + (nt, nz, ny, nx)) + meridian = np.zeros((nt, nz, ny, nx)) + + FiretoNC(path, domain_properties(nx, ny), parameters_properties(), + fuel, wind={"zonal": zonal, "meridian": meridian}) + + with Dataset(path) as ds: + got = np.array(ds.variables['windU'][:]) + + if got.shape != (nt, nz, ny, nx): + failures.append("windU has shape %s, expected %s" + % (got.shape, (nt, nz, ny, nx))) + elif not np.array_equal(got, zonal): + failures.append("windU values do not match the input") + + return failures + + +def test_three_dimensional_field(): + """A 3-D field gains a leading NT of 1 and keeps its values. + + The old code never created the NT dimension on this path, so writing a + 3-D field failed outright. + """ + failures = [] + nz, ny, nx = 2, 3, 4 + + with tempfile.TemporaryDirectory() as workdir: + path = os.path.join(workdir, "landscape.nc") + + fuel = np.full((ny, nx), 1, dtype=np.int32) + zonal = np.arange(nz * ny * nx, dtype=float).reshape((nz, ny, nx)) + + FiretoNC(path, domain_properties(nx, ny), parameters_properties(), + fuel, wind={"zonal": zonal, + "meridian": np.zeros((nz, ny, nx))}) + + with Dataset(path) as ds: + got = np.array(ds.variables['windU'][:]) + + if got.shape != (1, nz, ny, nx): + failures.append("windU has shape %s, expected %s" + % (got.shape, (1, nz, ny, nx))) + elif not np.array_equal(got[0], zonal): + failures.append("3-D windU values do not match the input") + + return failures + + +def test_missing_parameter_is_reported_before_writing(): + """A missing key names itself, and leaves no half-written file behind.""" + failures = [] + incomplete = parameters_properties() + del incomplete['refDay'] + + with tempfile.TemporaryDirectory() as workdir: + path = os.path.join(workdir, "landscape.nc") + try: + FiretoNC(path, domain_properties(), incomplete, + np.full((NY, NX), 1, dtype=np.int32)) + failures.append("a missing parameter key did not raise") + except KeyError as error: + if 'refDay' not in str(error): + failures.append("the error does not name the missing key: %s" + % error) + if os.path.exists(path): + failures.append("a file was written despite the missing key") + + return failures + + +def main(): + tests = [test_writes_a_loadable_landscape, + test_four_dimensional_field, + test_three_dimensional_field, + test_missing_parameter_is_reported_before_writing] + + failures = [] + for test in tests: + try: + result = test() + except Exception: + result = ["%s raised:\n%s" % (test.__name__, traceback.format_exc())] + for line in result: + if line.startswith("SKIP:"): + print("%s: %s" % (test.__name__, line)) + else: + failures.append("%s: %s" % (test.__name__, line)) + if not result: + print("%s: ok" % test.__name__) + + if failures: + print("\nFAILED with %d problem(s):" % len(failures)) + for failure in failures: + print(" - %s" % failure) + return 1 + print("\nOK") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/README.md b/tools/README.md index 189328c3..5fba5c96 100644 --- a/tools/README.md +++ b/tools/README.md @@ -9,7 +9,7 @@ to couple the simulation with MesoNH you should also edit the &NAM_FOREFIRE name List of preprocessing scripts: - ***clickImageToLocation.py*** sets the coordinates for the Init.ff file -- ***genForeFireCase.py*** contains routines for addFieldToNcFile +- ***genForeFireCase.py*** writes the **NetCDFfile** landscape (fuel, elevation, wind, flux models) from numpy arrays - ***genPrepIdeal.py*** creates the .nam for mesonh ideal case ??? - ***kmlDomain.py*** extracts kml files from a netcdf file - ***PGD2Init.py*** creates the **InitFile**=Init.ff in the **ForeFireDataDirectory** directory @@ -19,16 +19,25 @@ List of preprocessing scripts: ### genForeFireCase -Contains the preprocessing routine *FiretoNC*: -to be used in order to generate a packed data landscape data in cdf format for ForeFire. +Lives in `preprocessing/genForeFireCase.py` and contains the preprocessing +routine *FiretoNC*, used to generate a packed data landscape in cdf format for +ForeFire. + **Usage:** FiretoNC(filename, file name domainProperties, the domain extension (map matching forefire parameters SWx, SWy, SWz, Lx, Ly, Lz, t0, Lt) - parametersProperties, the other optional properties you may want to put in the list + parametersProperties, the simulation date and duration. All of date, duration, refYear, refDay, year, month and day are required fuelModelMap, a numpy integer array containing the indexes of fuel type elevation=None, a numpy real array with the elevation wind=None, a map with a "zonal" and "meridian" numpy real array values fluxModelMap=None): a map with a ("table" and "name" ) and fMap "data" numpy int array values containing indices to the corresponding flux model +Field arrays are indexed outermost axis first: `(NY, NX)`, or `(NZ, NY, NX)` +and `(NT, NZ, NY, NX)` when they vary with height or time — the same +convention `prealCF2Case.py` uses. + +`tests/python/test_genforefirecase.py` builds a landscape, loads it and runs a +short simulation; it doubles as a worked example. + ### PGD2Init diff --git a/tools/preprocessing/READMEscripts.md b/tools/preprocessing/READMEscripts.md index 28fa611d..3921526d 100644 --- a/tools/preprocessing/READMEscripts.md +++ b/tools/preprocessing/READMEscripts.md @@ -23,7 +23,7 @@ to couple the simulation with MesoNH you should also edit the &NAM_FOREFIRE name List of scripts: - ***clickImageToLocation.py *** sets the coordinates for the Init.ff file -- ***genForeFireCase.py*** contains routines for addFieldToNcFile +- ***genForeFireCase.py*** writes the **NetCDFfile** landscape (fuel, elevation, wind, flux models) from numpy arrays - ***genPrepIdeal.py*** creates the .nam for mesonh ideal case ??? - ***prealCF2Case.py*** creates the **NetCDFfile**=data.nc file in the **ForeFireDataDirectory** directory - ***pngs2bmap.py*** creates the burning map file **BMapFiles** starting from kml contours @@ -35,7 +35,7 @@ Contains the preprocessing routine *FiretoNC*: to be used in order to generate a packed data landscape data in cdf format for ForeFire. Usage: FiretoNC(filename, file name domainProperties, the domain extension (map matching forefire parameters SWx, SWy, SWz, Lx, Ly, Lz, t0, Lt) - parametersProperties, the other optional properties you may want to put in the list + parametersProperties, the simulation date and duration. All of date, duration, refYear, refDay, year, month and day are required fuelModelMap, a numpy integer array containing the indexes of fuel type elevation=None, a numpy real array with the elevation wind=None, a map with a "zonal" and "meridian" numpy real array values diff --git a/tools/preprocessing/genForeFireCase.py b/tools/preprocessing/genForeFireCase.py new file mode 100644 index 00000000..41bef68c --- /dev/null +++ b/tools/preprocessing/genForeFireCase.py @@ -0,0 +1,164 @@ +# Copyright (C) 2012 +# Author(s): Jean Baptiste Filippi, Vivien Mallet +# +# This file is part of pyFireScore, a tool for scoring wildfire simulation +# +# pyFireScore is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# pyFireScore is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. + +"""Writes the NetCDF landscape file ForeFire reads. + +ForeFire needs one file describing the terrain a fire runs on: which fuel +sits in each cell, the elevation, and optionally the wind and the flux models. +This builds it from numpy arrays. + + from genForeFireCase import FiretoNC + + FiretoNC("landscape.nc", domainProperties, parametersProperties, + fuelModelMap, elevation=..., wind=...) + +then point a ForeFire script at it with + + setParameter[NetCDFfile=landscape.nc] + +Field arrays are indexed the way ForeFire reads them, outermost axis first: +(NY, NX) for a 2-D field, (NZ, NY, NX) for a 3-D one, (NT, NZ, NY, NX) for a +4-D one. This is the convention prealCF2Case.py in this directory already +uses. + +Arguments to FiretoNC: + + filename: where to write. + domainProperties: the domain extent, matching the ForeFire parameters + SWx, SWy, SWz, Lx, Ly, Lz, t0, Lt. + parametersProperties: simulation date and duration. See REQUIRED_PARAMETERS. + fuelModelMap: integer array of fuel indices into the fuel table. + elevation: real array of ground elevation, metres. + wind: dict with "zonal" and "meridian" real arrays. + fluxModelMap: list of dicts, each with "name", "data" (integer array + of model indices) and "table" (name -> index). + +prealCF2Case.py in this directory carries its own copy of FiretoNC and +addFieldToNcFile, which it fixed independently while this file was missing. +The two should be merged, but that is a change to a working script and is left +for its own commit. +""" + +import numpy as np +from netCDF4 import Dataset + +# parametersProperties has no defaults: every one of these ends up as an +# attribute ForeFire reads back, and guessing a date for someone is worse than +# telling them which key is missing. +REQUIRED_PARAMETERS = ('date', 'duration', 'refYear', 'refDay', + 'year', 'month', 'day') + + +def addFieldToNcFile(ncfile, field, fieldname, typeName, dvartype): + """Writes one field as a (NT, NZ, NY, NX) variable. + + `field` is indexed (NY, NX), (NZ, NY, NX) or (NT, NZ, NY, NX): the same + order, with the leading axes dropped when they are not used. + + The dimensions used to be read off the array as (NY, NX, NZ, NT) while the + variable was created as (NT, NZ, NY, NX) and assigned without transposing, + so only the 2-D path was right. A 4-D field raised a broadcast error, and a + 3-D one never got its NT dimension created at all. + """ + sp = np.shape(field) + if len(sp) < 2 or len(sp) > 4: + raise ValueError( + "field '%s' has %d dimensions, expected 2, 3 or 4 " + "([NT, ][NZ, ]NY, NX)" % (fieldname, len(sp))) + + # Pad to (NT, NZ, NY, NX); the leading axes are 1 when not supplied. + nt, nz, ny, nx = (1,) * (4 - len(sp)) + tuple(sp) + + ncfile.createDimension('%sNX' % fieldname, nx) + ncfile.createDimension('%sNY' % fieldname, ny) + ncfile.createDimension('%sNZ' % fieldname, nz) + ncfile.createDimension('%sNT' % fieldname, nt) + + variable = ncfile.createVariable( + fieldname, dvartype, + ('%sNT' % fieldname, '%sNZ' % fieldname, + '%sNY' % fieldname, '%sNX' % fieldname)) + + variable[:, :, :, :] = np.reshape(field, (nt, nz, ny, nx)) + + variable.type = typeName + + return variable + + +def FiretoNC(filename, domainProperties, parametersProperties, fuelModelMap, + elevation=None, wind=None, fluxModelMap=None, bmap=None, + cellMap=None): + """Writes a ForeFire landscape file. See the module docstring.""" + + if parametersProperties is not None: + missing = [k for k in REQUIRED_PARAMETERS + if k not in parametersProperties] + if missing: + # Checked before the file is opened: a half-written landscape that + # ForeFire then fails to load is harder to diagnose than this. + raise KeyError( + "parametersProperties is missing %s. All of %s are required." + % (', '.join(missing), ', '.join(REQUIRED_PARAMETERS))) + + # NETCDF3_CLASSIC because that is what ForeFire has always been given here, + # and the reader on the C++ side is the legacy netcdf-cxx4 API. + ncfile = Dataset(filename, 'w', format='NETCDF3_CLASSIC') + + ncfile.version = "FF.1.0" + domain = ncfile.createVariable('domain', 'S1', ()) + domain.type = "domain" + domain.SWx = float(domainProperties['SWx']) + domain.SWy = float(domainProperties['SWy']) + domain.SWz = float(domainProperties['SWz']) + domain.Lx = float(domainProperties['Lx']) + domain.Ly = float(domainProperties['Ly']) + domain.Lz = float(domainProperties['Lz']) + domain.t0 = float(domainProperties['t0']) + domain.Lt = float(domainProperties['Lt']) + + parameters = ncfile.createVariable('parameters', 'S1', ()) + parameters.type = "parameters" + + if parametersProperties is not None: + parameters.date = parametersProperties['date'] + parameters.duration = parametersProperties['duration'] + parameters.refYear = parametersProperties['refYear'] + parameters.refDay = parametersProperties['refDay'] + parameters.year = parametersProperties['year'] + parameters.month = parametersProperties['month'] + parameters.day = parametersProperties['day'] + + if fuelModelMap is not None: + addFieldToNcFile(ncfile, fuelModelMap, 'fuel', 'fuel', 'i4') + + if elevation is not None: + addFieldToNcFile(ncfile, elevation, 'altitude', 'data', 'f8') + + if wind is not None: + addFieldToNcFile(ncfile, wind["zonal"], 'windU', 'data', 'f8') + addFieldToNcFile(ncfile, wind["meridian"], 'windV', 'data', 'f8') + + if fluxModelMap is not None: + for fMap in fluxModelMap: + fVar = addFieldToNcFile(ncfile, fMap["data"], fMap["name"], + 'flux', 'i4') + for entry in fMap["table"].keys(): + setattr(fVar, "model%dname" % fMap["table"][entry], entry) + fVar.indices = np.array(list(fMap["table"].values()), dtype='i4') + + print("writing ", filename) + ncfile.sync() + ncfile.close() From f2701250775ceaa6b76e0fb9829cb44039cb09a5 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 21:27:59 +0200 Subject: [PATCH 20/63] ci: add a blocking AddressSanitizer job ForeFire reports zero ASan errors today on both test paths, so the job can be blocking from the day it lands rather than after a cleanup. The point of turning it on now is to keep that property while the ownership work in #175 starts moving allocations around. -DFOREFIRE_SANITIZE= passes to -fsanitize= on the compile line, the executables and the shared library, and switches the optimisation flags to -g -O1 -fno-omit-frame-pointer: the default -O3 -flto -fomit-frame-pointer set makes the reports unreadable. Having it as a build option rather than raw CMAKE_CXX_FLAGS in the workflow means the same build is reproducible locally. The job runs the unit suite and runff under ASAN_OPTIONS=detect_leaks=0. Errors block; leaks do not, and that is deliberate. Nothing owns a PropagationModel (#159), so ASan reports every one as leaked, and a job that is permanently red is a job everyone ignores. The leak count still runs as an informational step so the figure stays visible, and it can be made blocking once #159 lands. Verified in ubuntu:24.04, which is what ubuntu-latest runs: build with -fsanitize=address clean unit suite, detect_leaks=0 4/4, 0 errors runff, detect_leaks=0 passes, KML and NetCDF match leak report, detect_leaks=1 exits 8, ~4.7 MB reported Negative control: reintroducing the double free fixed in #157 into ForeFireV1HeatFluxModel turns the job red with ERROR: AddressSanitizer: attempting double-free SUMMARY: AddressSanitizer: double-free in operator delete[](void*) so the job catches the class of bug this project has actually had. Closes #162 --- .github/workflows/sanitizers.yml | 81 ++++++++++++++++++++++++++++++++ CMakeLists.txt | 16 +++++++ TESTING.md | 29 ++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 .github/workflows/sanitizers.yml diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 00000000..aef0c395 --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,81 @@ +name: 🧼 Sanitizers (Linux) + +# AddressSanitizer over both test paths: the unit suite, which constructs and +# destroys every propagation and flux model one call at a time, and runff, +# which runs a full simulation with a reload and NetCDF and KML output. +# +# This is a blocking check from the day it lands, which is only possible +# because ForeFire reports zero ASan errors today — no use-after-free, no +# overflow, no double free, on either path. There is no backlog to clear +# first, and the point of turning it on now is to keep it that way. See #162. +# +# Leaks are a separate matter and are deliberately NOT blocking: nothing owns +# a PropagationModel (#159), so ASan reports every one as leaked. Making that +# fail the build would produce a permanently red job, which teaches everyone +# to ignore it. The leak check runs anyway, as an informational step, so the +# number stays visible and can be made blocking once #159 lands. +# +# A separate workflow rather than a job in main.yml because it needs its own +# build: install-forefire.sh builds with -O3 -flto -fomit-frame-pointer, which +# is the opposite of what a sanitizer wants. + +on: + push: + branches: + - "master" + - "dev" + pull_request: + branches: [ "master", "dev" ] + workflow_dispatch: + +jobs: + address-sanitizer: + name: AddressSanitizer + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + lfs: true # runff reads data.nc, which is stored in LFS + + - name: Install Dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends \ + build-essential cmake python3 python3-pip + sudo apt-get install -y --no-install-recommends \ + libnetcdf-dev libnetcdf-c++4-dev + pip3 install --no-cache-dir --break-system-packages lxml xarray netCDF4 + + - name: Build with AddressSanitizer + # FOREFIRE_SANITIZE also switches the compile flags to -g -O1 + # -fno-omit-frame-pointer, so the stack traces are readable. + run: | + cmake -S . -B build-asan -DFOREFIRE_SANITIZE=address + cmake --build build-asan -j"$(nproc)" + + # Errors are blocking. detect_leaks=0 is the whole reason this can be: + # see the note at the top about #159. + - name: Unit tests under ASan + env: + ASAN_OPTIONS: detect_leaks=0 + run: ctest --test-dir build-asan --output-on-failure + + - name: runff under ASan + env: + ASAN_OPTIONS: detect_leaks=0 + run: | + cd tests/runff + bash ff-run.bash + + # Informational only. Prints what is leaking so the figure stays visible + # without gating the build on a known ownership gap. + - name: Leak report (informational) + continue-on-error: true + env: + ASAN_OPTIONS: detect_leaks=1 + run: | + echo "::group::Unit suite leaks" + ctest --test-dir build-asan --output-on-failure || true + echo "::endgroup::" diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c402cf7..a11c7b19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,8 @@ option(FOREFIRE_CHECK_LFS "Run the Git LFS data integrity check at configure tim option(FOREFIRE_BUILD_TESTS "Build the C++ unit tests, registered with CTest" ${_ff_default_tests}) option(FOREFIRE_ENABLE_WARNINGS "Compile ForeFire's own sources with -Wall -Wextra" ON) option(FOREFIRE_WARNINGS_AS_ERRORS "Fail the build on a compiler warning" OFF) +set(FOREFIRE_SANITIZE "" CACHE STRING + "Sanitizers to build with, passed to -fsanitize= (e.g. address, undefined, address,undefined)") # ---------------------------------- # Set C++ Standard @@ -220,6 +222,11 @@ endif() # ---------------------------------- if(MPI_FOUND) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 -DMPI_COUPLING") +elseif(FOREFIRE_SANITIZE) + # A sanitizer build wants frame pointers and debug info; the release set + # below strips both, and -flto makes the reports harder to read besides. + # -O1 keeps the suites quick enough to run in CI. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O1 -fno-omit-frame-pointer") else() # uncomment if want to debug # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0") @@ -229,6 +236,15 @@ else() endif() endif() +# Sanitizers have to reach the link line as well as the compile line, and the +# shared library too: the core is where the allocations happen. +if(FOREFIRE_SANITIZE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=${FOREFIRE_SANITIZE}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=${FOREFIRE_SANITIZE}") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=${FOREFIRE_SANITIZE}") + message(STATUS "Sanitizers enabled: -fsanitize=${FOREFIRE_SANITIZE}") +endif() + # Warnings are a target property rather than a global flag on purpose: they # should apply to code this project writes, not to pybind11's headers or to # whatever a consumer compiles against the installed library. diff --git a/TESTING.md b/TESTING.md index c42ea85c..13d0f7fd 100644 --- a/TESTING.md +++ b/TESTING.md @@ -115,6 +115,35 @@ for the right reason. The `tests/` directory contains other subdirectories (`mnh_*`, `runANN`) for testing specific features like coupled simulations. A main `tests/run.bash` script exists but is not currently fully validated in CI. Refer to specific subdirectories for details if needed. +## Sanitizers + +`-DFOREFIRE_SANITIZE=` builds with `-fsanitize=`, applied to the +compile line, the executables and the shared library. It also switches the +optimisation flags to `-g -O1 -fno-omit-frame-pointer`, since the default +release set (`-O3 -flto -fomit-frame-pointer`) makes sanitizer reports hard to +read. + +```bash +cmake -S . -B build-asan -DFOREFIRE_SANITIZE=address +cmake --build build-asan -j +ASAN_OPTIONS=detect_leaks=0 ctest --test-dir build-asan --output-on-failure +``` + +`address` is what CI runs, on both the unit suite and `runff`, as a blocking +check. Other values are passed straight through — `undefined`, or +`address,undefined` for both — but only `address` is currently verified clean. + +**`detect_leaks=0` is deliberate, not a workaround.** ForeFire reports zero +ASan *errors* — no use-after-free, no overflow, no double free — on either test +path, which is what makes a blocking job possible. It does leak: nothing owns a +`PropagationModel` (#159), so every one is reported. Leaving leak detection on +would produce a permanently failing job that everyone learns to ignore. The CI +workflow runs the leak check anyway as an informational step, so the number +stays visible, and it can be made blocking once #159 lands. + +Note that the sanitizer build writes `bin/forefire` and `lib/libforefireL.so` +like any other build, so it replaces a release build in the source tree. + ## Compiler Warnings ForeFire's own sources compile with `-Wall -Wextra` by default. The warnings From 2673b4e45beaf1e8d0a1a56de40cdc49a4887b19 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 22:49:38 +0200 Subject: [PATCH 21/63] fix(tests): make runANN work, and run it in CI runANN has failed on line 2 since it was committed. It diffs the model predictions against result.txt.ref, and that file is not in the repository: diff: result.txt.ref: No such file or directory ANN results differs from reference. Renaming the committed result.txt would not have fixed it. Regenerating that output changes a handful of lines in the last digit (12412.1 against 12412.2), so an exact diff is red on any machine but the one that produced the file. ANN_test already computes the root mean squared error between what the network predicts and what the model produced, and run.bash was throwing that line away with `grep result`. It is now what the test checks: no reference file, no dependence on the last digit, and it fails when the network stops approximating the model. The tolerance is calibrated against this fixture: trained network 0.0235 predicting the mean of every output 0.0966 normalisation weights scaled by 1% 17817 0.05 sits between the first two, so a network that ignores its inputs fails rather than passing. Anything that genuinely breaks the arithmetic lands orders of magnitude away. The fixture is weak, and that is recorded rather than papered over: the expected outputs take four distinct values spanning 1.1 in 12412, which is why the margin between a working network and a constant one is so narrow. Better inputs would make this a much stronger check. Wired into main.yml. This is the only coverage for ANNPropagationModel and BMapLoggerForANNTraining: both read a .ffann network in their constructor and abort without one, so the unit suite cannot construct them. ANN_test is already built there, since FOREFIRE_BUILD_TOOLS defaults on outside wheel builds. Also: - result.txt is removed. It was the golden file for a comparison that no longer exists, and ANN_test regenerates that output on demand with its `print` argument. - clean.bash deleted results.txt, which is not the name of anything the suite writes. There is nothing to clean now. - TESTING.md said runff was the only test validated in CI, which has not been true since the unit suite and the moisture invariants landed. Verified in ubuntu:24.04: same RMSE as the host to seven digits, which is the portability this replaces the exact diff for. Closes #163 --- .github/workflows/main.yml | 11 +- TESTING.md | 51 +- tests/runANN/clean.bash | 9 +- tests/runANN/result.txt | 1424 ------------------------------------ tests/runANN/run.bash | 81 +- 5 files changed, 140 insertions(+), 1436 deletions(-) delete mode 100644 tests/runANN/result.txt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f33b045c..78ad1131 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,4 +68,13 @@ jobs: - name: Run 'runff' Test Script run: | cd tests/runff - bash ff-run.bash # Execute the complete test logic script \ No newline at end of file + bash ff-run.bash # Execute the complete test logic script + + # The only coverage for ANNPropagationModel and BMapLoggerForANNTraining: + # both read a .ffann network in their constructor and abort without one, + # so the unit suite cannot reach them. ANN_test is built by the step + # above, since FOREFIRE_BUILD_TOOLS defaults on outside wheel builds. + - name: Run 'runANN' Test Script + run: | + cd tests/runANN + bash run.bash \ No newline at end of file diff --git a/TESTING.md b/TESTING.md index 13d0f7fd..b6b02689 100644 --- a/TESTING.md +++ b/TESTING.md @@ -32,7 +32,7 @@ They need no Python and no test data. Configure with ## Running the Core Test (`runff`) -The primary automated test, validated in our CI pipeline, is located in `tests/runff/`. This test verifies core simulation, save/reload functionality, and NetCDF/KML output generation against reference files. +The primary physics regression test, validated in CI by `main.yml` and `macos.yml`, is located in `tests/runff/`. This test verifies core simulation, save/reload functionality, and NetCDF/KML output generation against reference files. **To run this test manually:** @@ -111,9 +111,56 @@ which is the point — it is the failing test the work in #175 has to make pass. Wiring it into CI belongs with the last step of that issue, once it can pass for the right reason. +## Running the ANN Test (`runANN`) + +Validated in CI by `main.yml`. It is the only coverage for +`ANNPropagationModel` and `BMapLoggerForANNTraining`: both read a `.ffann` +network in their constructor and abort without one, so the C++ unit suite +cannot construct them. + +It runs the trained network in `Rothermel.ffann` over the 1424 inputs in +`modelrun.csv` and checks the root mean squared error against what the +propagation model produced. + +**To run it manually**, after building (`ANN_test` needs +`-DFOREFIRE_BUILD_TOOLS=ON`, which is the default outside wheel builds): + +```bash +cd tests/runANN +bash run.bash +``` + +Add `print` to `ANN_test` for a per-input dump: + +```bash +../../bin/ANN_test Rothermel.ffann modelrun.csv print +``` + +The tolerance is calibrated between a working network and a useless one: + +| | RMSE | +| --- | --- | +| trained network | 0.0235 | +| predicting the mean of every output | 0.0966 | +| normalisation weights scaled by 1% | 17817 | +| **tolerance** | **0.05** | + +Note the fixture is weak: the expected outputs take four distinct values +spanning 1.1 in 12412, which is why the gap between a working network and a +constant one is so narrow. Replacing `modelrun.csv` with inputs that produce a +real spread of rates of spread would make this a much stronger check. + ## Other Tests -The `tests/` directory contains other subdirectories (`mnh_*`, `runANN`) for testing specific features like coupled simulations. A main `tests/run.bash` script exists but is not currently fully validated in CI. Refer to specific subdirectories for details if needed. +`tests/run.bash` runs every suite that its environment allows: `runff` and +`runANN` always, the `mnh_*` coupled cases when `SRC_MESONH` is set, and +`tests/python/` when `PYTHONEXE` is set. It is not itself invoked by CI, which +runs the suites individually. + +`tests/python/` also holds `idealizedwind.py`, `farsite_flat.py` and +`percolation.py`. They are examples rather than tests — they produce plots and +assert nothing — and are not run anywhere. They are the closest thing to +worked examples in the repository, so keeping them executable is worthwhile. ## Sanitizers diff --git a/tests/runANN/clean.bash b/tests/runANN/clean.bash index 58fc1581..0c93c3b7 100755 --- a/tests/runANN/clean.bash +++ b/tests/runANN/clean.bash @@ -1 +1,8 @@ -rm -f results.txt +#!/bin/bash +# run.bash writes nothing to disk any more — it reads the RMSE off ANN_test's +# stdout — so there is nothing to clean. Kept because tests/run.bash calls a +# clean.bash in each suite directory. +# +# The old result.txt this used to remove was named results.txt here, so it +# never matched anything either. +exit 0 diff --git a/tests/runANN/result.txt b/tests/runANN/result.txt deleted file mode 100644 index 0e07bc60..00000000 --- a/tests/runANN/result.txt +++ /dev/null @@ -1,1424 +0,0 @@ -[0.0159606 1.60695 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0150665 1.6071 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00477084 4.19494 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0120212 -1.2461 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00689611 -2.80406 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0162241 1.42026 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0152087 1.76584 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00368188 4.22922 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00704528 2.79705 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00926602 -4.12085 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0117532 -1.24658 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00547048 2.90768 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00597333 -2.98328 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0103928 -4.09107 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0153257 1.89549 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00820509 2.80483 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0060686 3.0728 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0115311 -1.33834 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00899401 -4.08662 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00576692 2.82613 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00610251 -2.95312 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0166917 0.917398 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0100564 -4.1425 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.016478 1.2668 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00243581 4.25032 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0142097 2.17351 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00981689 -4.08845 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0155268 1.90528 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00779886 2.92512 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00599797 3.10573 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0112767 -1.35324 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00933372 -4.07236 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00603219 2.74395 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00591612 -2.98182 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0167988 0.682262 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0102528 -4.14673 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0167417 1.2266 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.001955 4.25214 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0140638 2.37208 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0101391 -4.07784 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0157315 1.91001 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00751259 3.01409 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00552615 3.11911 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985803 -4.02835 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00417414 2.66842 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00677512 2.11155 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0169086 0.488507 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0102559 -4.16559 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00861665 -4.24735 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0170098 1.2084 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00436997 4.25267 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0139561 2.50422 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0104025 -4.07348 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0110179 -1.35678 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00573102 -3.00847 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0159376 1.91272 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000296474 3.08387 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00814895 3.12224 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0053318 2.64933 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0026493 1.95385 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0171079 0.396929 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0104647 -4.16906 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00835347 -4.2531 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0172799 1.19962 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00875103 4.25275 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0184902 2.59912 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0107535 -1.3499 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0102532 -3.79244 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0057725 -2.99457 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0109021 -4.037 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0100554 -4.03075 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0178375 1.915 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00849425 3.20527 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00527147 2.6395 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00175403 1.8437 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0106992 -4.17077 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0082278 -4.25336 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.017551 1.2631 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00981126 3.24301 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0191502 2.6792 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0191469 3.09535 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0104884 -1.3431 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0104643 -3.77067 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00564738 -3.00964 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0162754 -0.230293 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.010818 4.25119 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0108623 -4.07181 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0103106 -4.02369 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00923734 3.09847 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0173594 0.355028 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0189494 1.9182 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00827074 3.11883 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00522517 2.63461 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00135592 1.76267 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0109479 -4.17151 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00821559 -4.25139 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0178203 1.19421 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0108757 3.26823 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.019011 2.7006 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0188827 3.17562 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0102225 -1.33643 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0107428 -3.71966 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00551754 -3.02467 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0164563 -0.285487 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0212872 4.25038 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0110839 -4.07235 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0105416 -4.02014 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00933881 3.11136 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0176307 0.330845 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0187398 1.92077 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00831936 3.11514 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00518491 2.63185 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000999551 1.70002 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0112028 -4.17182 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00827481 -4.24864 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0180919 1.19235 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0111861 3.33216 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0188972 2.7105 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0186717 3.24057 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00804817 -1.3304 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0109646 -3.67902 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00539274 -3.03807 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.016534 -0.386568 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0209934 4.25042 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0113048 -4.07302 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0107615 -4.01831 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00942283 3.11812 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0179175 0.320039 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[0.0185307 1.92311 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00836656 3.11136 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00514638 2.62979 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000670257 1.64964 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0114592 -4.17204 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0083817 -4.24569 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0179571 1.19106 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0114609 3.38017 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0187957 2.71465 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0185238 3.28839 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0111801 -3.63006 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0165847 -0.492291 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0206977 4.25052 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00752614 -1.32547 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00119864 1.25277 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0052781 -3.04915 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00667263 -4.2029 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0115259 -4.07372 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0109768 -4.01715 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00949869 3.12204 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0182119 0.315571 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0183222 1.92525 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0084129 3.10764 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00510751 2.62777 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000470807 1.64338 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0117155 -4.17227 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00865966 -4.24532 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0174067 1.19008 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0117138 3.41784 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0186995 2.71618 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.01842 3.32534 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0167859 -0.5264 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0204013 4.25063 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00777484 -1.31735 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0106832 -3.3085 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00168386 1.15953 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00530308 -3.03789 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00658119 -4.18713 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0117473 -4.07442 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0112567 -4.00315 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00957138 3.12478 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0185092 0.313538 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[-0.0112681 -3.63711 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.018114 1.92721 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00845863 3.10406 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00506849 2.62586 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000285482 1.64187 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0119718 -4.17252 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0089533 -4.24521 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0171349 1.18926 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0186054 2.71666 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0183481 3.35498 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0201367 4.24991 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0151263 3.70768 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00803494 -1.31536 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0107168 -3.29719 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00211553 1.08906 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00519429 -3.04775 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0150874 -1.05167 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00657226 -4.17356 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0119705 -4.07491 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0114029 -4.01505 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00977749 3.16233 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0187801 0.28969 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[-0.0113847 -3.62924 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.011894 3.43275 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.017026 -0.542586 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00850442 3.10076 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00502916 2.624 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000110516 1.64363 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0122282 -4.17277 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00925902 -4.24532 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.016863 1.18837 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0173743 1.59783 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.018532 2.70298 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0176014 3.55971 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0182331 3.61859 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0201889 4.22845 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0150457 3.72342 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00829258 -1.3118 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0107662 -3.25518 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00251207 1.03374 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00510415 -3.05431 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0152682 -1.07096 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00662546 -4.16215 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0121928 -4.07558 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0116146 -4.01434 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00971912 3.12929 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0191068 0.311991 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[-0.0114905 -3.62626 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0120589 3.4424 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0172706 -0.556285 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0179063 1.92899 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00854923 3.09747 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00498945 2.62216 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-5.97604e-05 1.64669 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0124846 -4.17304 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00956968 -4.24551 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0165911 1.18639 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0171338 1.59703 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0184136 2.71951 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0176513 3.56753 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181361 3.65778 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0199142 4.22773 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0151071 3.76073 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00855031 -1.30808 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0107951 -3.22129 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00288424 0.989042 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00500779 -3.06148 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0153103 -1.13168 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00672096 -4.15244 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0124159 -4.07617 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0118259 -4.01365 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00979075 3.13117 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0175508 0.311986 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.011592 -3.62539 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.012207 3.44698 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0175353 -0.561236 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[0.0176985 1.93165 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00859379 3.09434 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00494933 2.62034 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000228245 1.65012 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0127409 -4.17331 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00988176 -4.24573 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0163189 1.18393 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.016893 1.59633 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0183165 2.72273 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0177229 3.5708 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0180847 3.68891 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.019634 4.22765 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0150979 3.78897 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00880899 -1.30471 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0108095 -3.19326 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00323912 0.952002 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00491306 -3.06808 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0153822 -1.18002 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00684684 -4.14408 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0126395 -4.07673 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0120368 -4.01297 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00986249 3.13294 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0156963 0.312066 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.011692 -3.62542 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0123473 3.4493 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0178091 -0.562372 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0174909 1.93499 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00863803 3.09136 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00490881 2.61853 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000396356 1.65346 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0129973 -4.1736 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0101936 -4.24594 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0160465 1.18126 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0166522 1.59569 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0182204 2.72576 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0178062 3.57147 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0202142 3.71327 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0193506 4.22791 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0150402 3.8111 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.010815 -3.15659 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00363309 0.90291 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00494467 0.583915 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0154105 -1.23809 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00691927 -4.13261 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00503847 -4.04523 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00903926 -1.28322 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00418034 -3.08694 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0128636 -4.07726 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0122473 -4.0123 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00993438 3.13461 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0153965 0.312169 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0117917 -3.62576 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0124843 3.45065 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0180868 -0.561889 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[0.0172837 1.9385 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00868196 3.0885 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00486789 2.61675 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000565273 1.6564 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0132538 -4.17389 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0105041 -4.24612 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0157738 1.17855 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0164112 1.59511 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181253 2.72861 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0178954 3.57069 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0209408 3.73301 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0190652 4.22835 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.014948 3.829 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00391018 0.89519 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00527583 0.562496 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0155794 -1.25376 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00716676 -4.13073 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00520208 -4.03856 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00929812 -1.27875 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0100949 -2.88209 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00165694 -3.07961 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0130882 -4.07776 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0124576 -4.01164 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0100064 3.1362 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0150967 0.312279 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0119055 -3.61296 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0126203 3.45164 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0183657 -0.560947 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[0.0170772 1.94204 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0108064 -3.16288 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0087256 3.08577 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00482657 2.61498 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000734168 1.65919 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0135104 -4.17419 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.010814 -4.2463 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0155008 1.17586 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0161702 1.59459 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0180311 2.73128 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0179874 3.5691 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0208286 3.74936 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0183976 4.25038 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0153209 3.97838 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0156196 3.95898 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00417202 0.892871 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0056721 0.516694 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0156321 -1.28215 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00743722 -4.13015 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00525422 -4.02378 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00956303 -1.27794 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0101543 -2.8723 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0017663 -3.07126 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0133132 -4.07823 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0126679 -4.0109 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0100787 3.13771 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0147969 0.312382 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0119901 -3.62628 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0130725 3.45259 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0186453 -0.559762 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[0.0168712 1.94554 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0106885 -3.1575 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00876895 3.08316 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00384319 2.39299 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00165371 2.07312 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-1.75247e-05 1.90115 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0131004 -4.21794 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0122314 -4.24922 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.012044 -4.25366 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0152276 1.17322 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.015929 1.59411 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0179377 2.73378 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0180805 3.56705 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0207111 3.76915 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181291 4.24975 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0152753 4.00436 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0154096 3.96394 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00442694 0.892928 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00604933 0.479249 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00771765 -4.13013 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0055179 -4.01117 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00982634 -1.27602 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00998629 -2.84035 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00179001 -3.07559 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00842907 -1.66454 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0135358 -4.07914 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0128775 -4.01025 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0101512 3.13917 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0144971 0.312472 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0101151 -3.6268 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0133862 3.45574 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0188721 -0.578947 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[0.0166659 1.94897 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0099985 -3.15591 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00928313 -1.28858 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00879529 3.07708 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00376755 2.3922 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00154543 2.07665 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.000154322 1.9038 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0133752 -4.21807 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0125365 -4.24903 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0123513 -4.25365 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0156877 1.59162 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0182356 3.54695 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0205817 3.78433 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0152154 4.02896 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0151802 3.96631 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00466655 0.89798 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00641237 0.448071 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00802001 -4.13131 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00511653 -4.00052 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00854654 -1.27395 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00991858 -2.81389 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00182652 -3.07813 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00821463 -1.67273 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0149541 1.17065 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0145766 0.763306 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0178611 4.24906 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.01958 3.66307 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0137553 -4.08064 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0123975 -4.00963 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.010224 3.14061 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0141971 0.312126 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00985319 -3.62724 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0132699 3.45878 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0192027 -0.557948 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.1 observed 12412.1 -[0.0164618 1.94004 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0172363 2.24611 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999851 -3.15595 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00903551 -1.29503 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0178354 2.74679 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00882629 3.07235 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00145353 2.08401 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.000295727 1.90513 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0136529 -4.21805 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0128432 -4.24881 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0126539 -4.25364 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0154465 1.59315 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0150462 4.04037 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0149397 3.96717 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00490487 0.903255 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00676465 0.421672 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00690937 -4.13259 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00474074 -3.99135 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00854152 -1.27204 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00990951 -2.79155 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00185629 -3.08184 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00788978 -1.70624 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0146808 1.16972 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0142844 0.762836 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0175569 4.24948 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0195502 3.75254 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00369644 2.39249 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00779164 3.06733 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0139761 -4.08203 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00928496 -4.00913 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0102971 3.14201 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0138965 0.310479 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00985282 -3.62765 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0131519 3.46166 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00953973 -0.55698 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0162562 1.94149 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0170638 2.27232 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999855 -3.1567 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00880841 -1.29684 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0177636 2.7265 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0183247 3.11156 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.018257 3.56438 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00887996 3.07255 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00133412 2.08434 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.000440111 1.90564 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.013932 -4.21796 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0131485 -4.24861 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0129537 -4.25364 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0152051 1.59266 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0148837 4.05291 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0146938 3.96728 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00514293 0.908416 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00710873 0.39895 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00617547 -4.13383 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00438519 -3.98336 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00853953 -1.27041 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00990139 -2.77236 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00188654 -3.08565 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00759462 -1.73418 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0144076 1.16955 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0139922 0.762372 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0172551 4.2498 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00361052 2.38944 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00779887 3.05824 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0194318 3.77993 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0203947 3.79082 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0141982 -4.08333 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00928654 -4.00848 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0103705 3.14339 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0135957 0.308212 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00985247 -3.62803 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.013032 3.46438 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0055518 -0.556023 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0160507 1.94213 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0169002 2.27458 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0099986 -3.15771 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00858988 -1.29666 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0176707 2.72812 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0182387 3.15434 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.018475 3.52216 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00893111 3.07222 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00122734 2.08764 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.000586163 1.9057 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0142116 -4.21783 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0134525 -4.24841 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0132518 -4.25364 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0149636 1.59221 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0146992 4.06301 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0144447 3.9669 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00538121 0.913316 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00744641 0.379118 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00591505 -4.13499 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00404573 -3.97634 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0085379 -1.26905 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00989406 -2.75562 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00191976 -3.08916 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00732246 -1.75788 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0141344 1.16974 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0136999 0.761916 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0169553 4.25005 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00352462 2.38655 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00780763 3.05 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0192972 3.79941 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0202517 3.78977 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999147 -4.08474 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0104443 3.14473 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0132948 0.305688 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00986155 -3.61594 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00964721 -3.82131 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0129105 3.46694 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00555358 -0.555107 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0158453 1.94237 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.016737 2.27667 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999865 -3.15878 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00837499 -1.29559 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0175786 2.72894 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0182288 3.15456 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0185492 3.52205 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00926714 -4.01576 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00898311 3.07208 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00111966 2.09056 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.000733561 1.90543 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0144915 -4.21769 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0137553 -4.24823 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0135487 -4.25365 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0147221 1.5918 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0144979 4.07132 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0141938 3.96622 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00561992 0.917903 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00777901 0.361601 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00565296 -4.13609 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00371928 -3.97009 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00853658 -1.26794 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00988739 -2.74083 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00702367 -1.78868 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0138613 1.1701 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0134076 0.761467 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0166573 4.25024 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00343929 2.38395 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00781777 3.0425 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0191527 3.81567 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0201093 3.78835 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00195654 -3.09224 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00347898 -3.89191 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00905047 -4.08598 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0105183 3.14605 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0129938 0.303085 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00986182 -3.6155 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0096174 -3.84172 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0127875 3.46936 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00555525 -0.554242 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0156399 1.94242 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0165743 2.27861 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0099987 -3.15966 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00816135 -1.29418 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0174869 2.72937 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0182191 3.15469 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.018623 3.52203 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00926751 -4.01559 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00903553 3.07203 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00101116 2.09313 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00088202 1.90492 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0140695 -4.24785 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0138447 -4.25365 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0144806 1.59141 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0142839 4.07826 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0139419 3.96536 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00585915 0.922166 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00895702 0.0187194 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00643285 0.0903122 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00538916 -4.13711 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00345994 -3.96912 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00814699 -0.977843 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00988119 -2.7274 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00682903 -1.7963 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0135883 1.17052 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0131153 0.761026 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0163607 4.25039 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00335481 2.38165 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00782907 3.03565 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0190007 3.82946 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0199675 3.78687 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00204097 -3.08753 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00310852 -3.87844 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891677 -4.11997 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00896296 -4.10921 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0105926 3.14735 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00986237 -3.61469 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00961753 -3.84158 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.012663 3.47165 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00553076 -0.566641 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00629224 -0.165927 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0154345 1.94238 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0164119 2.28042 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999874 -3.16044 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00794762 -1.29273 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0173954 2.72962 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0182093 3.15481 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0186967 3.52204 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00926711 -4.0157 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0150668 -4.19943 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0126984 0.313857 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00908818 3.07202 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000901928 2.09541 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00103138 1.90422 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.014367 -4.24775 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0118419 -4.25378 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0142389 1.59105 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0140599 4.08415 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0136896 3.96438 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00610923 0.922993 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00829277 0.0091588 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00650015 0.0528475 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00512405 -4.13807 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00320967 -3.96888 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00853411 -1.26586 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00974626 -2.4872 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00974483 -2.48503 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00660237 -1.81186 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0133153 1.17096 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.012823 0.760735 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0160653 4.25051 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00327124 2.37966 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00784131 3.02935 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0188427 3.84133 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0198263 3.78542 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00212724 -3.08225 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00276348 -3.86682 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0089177 -4.11973 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00919834 -4.04094 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0106671 3.14861 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00986304 -3.6137 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00961766 -3.84143 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0125372 3.47381 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00552939 -0.567318 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634142 -0.138917 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.015229 1.94232 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0162498 2.2821 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999877 -3.16106 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00773328 -1.29138 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0173039 2.7298 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181996 3.15494 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0187704 3.52205 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00926678 -4.01578 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0124665 -4.19938 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0118652 0.312798 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00914092 3.07203 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000792047 2.09744 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00245485 1.60132 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00435201 1.27156 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0146751 -4.24745 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00760361 -4.24203 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00636305 -4.20532 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0139972 1.59072 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0138278 4.08921 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0134372 3.96333 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00528388 1.23652 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00658807 0.00329726 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00658732 0.00388413 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00633343 -4.20301 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00296575 -3.96915 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00853329 -1.26514 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00971766 -2.444 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973925 -2.47639 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00638634 -1.82556 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0130423 1.1714 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0125307 0.760482 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0157708 4.2506 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00318858 2.37796 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00785432 3.02354 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0186798 3.85167 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0196857 3.78406 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00221079 -3.07715 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00243882 -3.8567 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00918729 -4.04462 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.010742 3.14985 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00986375 -3.61266 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0096178 -3.84129 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0124101 3.47586 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00552665 -0.568689 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.006342 -0.138573 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0150235 1.94225 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0160881 2.28367 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0099987 -3.15952 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00751833 -1.29013 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0172125 2.72994 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181899 3.15506 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0188441 3.52206 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00934811 -3.98364 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0084631 -4.20006 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712454 0.312504 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0091937 3.07204 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000653092 2.09304 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00265731 1.59727 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00453955 1.28063 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0149605 -4.24756 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00728173 -4.24173 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00611194 -4.20681 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0137555 1.59041 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0124982 3.84355 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0125857 3.699 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00549734 1.23981 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00658733 0.00374778 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00665717 -0.035695 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00606917 -4.20414 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00274142 -3.97107 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00853074 -1.26307 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00969265 -2.40765 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973614 -2.47157 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0127693 1.17183 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0122384 0.760267 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0127371 4.04499 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00310676 2.37651 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00786795 3.01815 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0185106 3.86373 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0176424 3.92883 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0195456 3.78279 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00225205 -3.07924 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00611788 -2.07559 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0021305 -3.8478 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0155078 4.24986 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0108171 3.15106 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00986448 -3.61159 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00961852 -3.84076 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0130282 3.6418 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0055233 -0.570367 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634244 -0.138301 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.014818 1.94218 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0159267 2.28514 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999864 -3.158 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00725611 -1.30104 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0171211 2.73008 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181803 3.15518 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0189177 3.52206 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00933012 -3.99104 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00903297 -4.09057 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00816008 -4.2304 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712452 0.31253 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0135611 4.09004 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00621643 -1.82862 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00909392 -4.0738 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00924649 3.07205 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000510994 2.08818 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00285619 1.59423 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00473116 1.28826 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0152298 -4.24796 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00696472 -4.2415 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00585302 -4.20804 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0135137 1.59012 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0122455 3.83736 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0124124 3.70166 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00571535 1.2417 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00658421 0.00554637 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00671581 -0.069171 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00579822 -4.20504 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00251646 -3.97299 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00852772 -1.26064 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00967083 -2.37689 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973449 -2.46896 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0124964 1.17224 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0119461 0.760074 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00302608 2.37536 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00788209 3.01314 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0183449 3.86641 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0174507 3.93753 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0194059 3.78165 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00229757 -3.08068 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.005976 -2.08031 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00183666 -3.84002 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.015224 4.24968 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0109061 3.15473 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0098652 -3.61053 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00961913 -3.84029 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0128655 3.64422 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00551956 -0.572243 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634282 -0.13807 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0146124 1.94212 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0157655 2.28651 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999857 -3.15653 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00708661 -1.28811 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0170297 2.73022 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181706 3.1553 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0189914 3.52205 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00933001 -3.99104 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891029 -4.12127 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00844219 -4.20266 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712474 0.312708 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0133168 4.09379 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00604695 -1.83172 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0090801 -4.0778 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00929931 3.07206 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000369791 2.08373 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00305326 1.59173 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00492437 1.29518 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00933727 -4.24842 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00665052 -4.24131 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00559087 -4.20916 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0132719 1.58984 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0120185 3.83557 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0122369 3.70403 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00593604 1.24279 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00657957 0.00820324 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00676562 -0.0977854 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00552316 -4.2058 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0022902 -3.97485 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00852475 -1.25825 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00965169 -2.35057 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973371 -2.46767 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0122235 1.17264 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0116538 0.759899 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00294649 2.37447 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00789664 3.00846 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181797 3.86777 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0172506 3.94905 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0192667 3.78058 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00233852 -3.08303 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00577786 -2.09904 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00155446 -3.83313 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.014398 4.25205 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0124319 4.06074 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0109942 3.15801 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00962145 -3.83868 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.012699 3.64596 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00551596 -0.57405 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634312 -0.137873 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0144068 1.94207 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0156046 2.28781 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999791 -3.14436 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00998112 -3.35915 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00687018 -1.28712 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0169383 2.73037 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181609 3.15542 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.019065 3.52203 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932977 -3.9911 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891042 -4.12122 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00843999 -4.20293 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712508 0.312948 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0136243 4.16342 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00588743 -1.83246 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00907026 -4.0806 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985967 -3.61796 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00935216 3.07207 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00022943 2.07966 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00324946 1.58954 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00511903 1.30146 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00702694 -4.24864 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00633758 -4.24114 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00532579 -4.21019 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0130301 1.58959 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0117941 3.83417 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0120594 3.70617 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00615846 1.24338 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00657391 0.0114338 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00680837 -0.122475 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00524533 -4.20646 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00206239 -3.97662 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00852204 -1.25606 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0096348 -2.32781 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973342 -2.46714 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0119506 1.17301 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0113615 0.759743 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00286789 2.37381 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00791154 3.00407 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0180147 3.86848 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0170395 3.96332 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0191279 3.77957 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00237909 -3.08557 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0055936 -2.11528 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00128194 -3.827 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0140948 4.25204 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0122678 4.07347 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0110825 3.16114 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00962016 -3.8395 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0125302 3.64729 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00551239 -0.575836 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634337 -0.137707 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0142012 1.94203 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0154439 2.28903 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999778 -3.14225 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00997617 -3.379 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00566021 -1.28618 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0168469 2.73053 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181513 3.15554 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0191386 3.522 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932956 -3.99115 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0089106 -4.12116 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00843814 -4.20315 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712544 0.313208 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0133337 4.16319 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00573223 -1.83213 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00906099 -4.08321 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985994 -3.61754 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00940502 3.07208 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[8.98395e-05 2.07592 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00344524 1.58753 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00531501 1.30715 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00160706 -4.24888 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00602545 -4.24098 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00505809 -4.21113 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0127882 1.58935 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0115718 3.83308 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0118804 3.70811 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00638209 1.24361 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00656754 0.0150638 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00684539 -0.143955 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00496546 -4.20705 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00183297 -3.9783 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00851962 -1.2541 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00961981 -2.30795 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973351 -2.46717 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0116777 1.17338 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00836807 0.759621 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00279017 2.37334 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00792671 2.99994 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0178499 3.8689 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0168254 3.97492 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0189894 3.77863 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00242092 -3.08802 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00542069 -2.12951 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00101753 -3.82151 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0137952 4.25209 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0120853 4.08417 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0111708 3.16413 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00962058 -3.83916 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0123599 3.64837 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00550892 -0.577576 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634358 -0.137567 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0139956 1.94201 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0152833 2.29019 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999763 -3.13985 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00997608 -3.37928 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00401236 -1.2853 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0167556 2.73069 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181416 3.15566 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0192122 3.52196 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932935 -3.9912 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891081 -4.1211 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00843656 -4.20335 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712581 0.313466 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0130406 4.16267 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00557881 -1.83131 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00905225 -4.08563 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985995 -3.61744 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00945789 3.0721 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-4.90561e-05 2.07249 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00364082 1.58564 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00551219 1.31233 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00159973 -4.24901 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00215631 -4.2409 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00478803 -4.212 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0125463 1.58913 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0113511 3.83223 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0116999 3.70987 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00660667 1.2436 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00656065 0.0189797 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0068777 -0.162775 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00468397 -4.20759 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00160199 -3.97988 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0085175 -1.25238 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00960643 -2.29047 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973385 -2.46759 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0114048 1.17373 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00782579 0.759495 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00271327 2.37305 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00794211 2.99606 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0176852 3.86919 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0166089 3.98465 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0188514 3.77773 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00246449 -3.09027 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00525713 -2.14211 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000759961 -3.81654 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0134978 4.25217 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0118879 4.09329 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00962098 -3.83884 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0122496 3.66434 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0116945 3.28565 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00633912 -0.139993 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.01379 1.94199 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.015123 2.29129 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999746 -3.13735 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00997599 -3.37951 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00398634 -1.29681 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0046779 -0.98194 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0166642 2.73087 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.018132 3.15577 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0192858 3.52191 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932916 -3.99125 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891103 -4.12103 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00843523 -4.20351 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712614 0.313706 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0127466 4.16203 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00542598 -1.83029 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00904401 -4.08789 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985987 -3.61749 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0111404 3.14704 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00553108 -0.566351 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00951031 3.07203 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000187328 2.06933 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00383631 1.58384 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00571047 1.31705 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00159291 -4.24913 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000209306 -4.24076 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00451581 -4.2128 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0123043 1.58892 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0111315 3.83156 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0115195 3.7118 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00793286 0.951739 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00552308 0.571467 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00602223 0.313257 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00690587 -0.179239 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00440113 -4.20809 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00136954 -3.98138 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00851565 -1.25085 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00959442 -2.27498 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973439 -2.46829 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.011132 1.17407 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00782555 0.759379 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00263711 2.37291 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0079577 2.99239 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0175204 3.86944 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0163906 3.99296 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0187136 3.77687 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00250982 -3.0923 614 613 0.1 1 4287 5738 0.4 1.378 0.174 8.3 1 300 70000 1.8727e+07 1.8727e+07 1800 600 0.3 2.5e-05 4 ] result: 12411.2 observed 12411.2 -[0.0051014 -2.15338 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00050821 -3.81202 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0132016 4.25227 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0116786 4.10115 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00962135 -3.83854 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0120793 3.66644 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0114768 3.25266 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634409 -0.137232 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0135844 1.94198 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0149629 2.29233 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999729 -3.13487 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00997592 -3.37969 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00398594 -1.29699 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00473273 -0.956183 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0165729 2.73105 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181224 3.15589 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0193593 3.52186 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932897 -3.99129 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891127 -4.12096 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00843409 -4.20366 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712645 0.313929 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0124523 4.16134 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00527282 -1.8293 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00903622 -4.09 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985974 -3.61759 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0112145 3.14806 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00552985 -0.566961 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00956275 3.07197 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000325039 2.06642 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00403176 1.58212 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00594328 1.31327 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00158654 -4.24924 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000216955 -4.24061 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00424167 -4.21354 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0120623 1.58872 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0109128 3.83102 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0113379 3.7136 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00761219 0.949368 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00551583 0.575138 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00600997 0.319789 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00691988 -0.187426 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00411711 -4.20855 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0011357 -3.98279 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00851402 -1.24951 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0095836 -2.26116 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973507 -2.46922 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0108592 1.1744 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00782532 0.759274 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00256163 2.37289 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00797342 2.98892 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0173556 3.8697 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0161708 4.00017 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0185762 3.77606 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00493184 -2.16895 614 613 0.1 1 4287 5738 0.4 1.378 0.174 8.3 1 300 70000 1.8727e+07 1.8727e+07 1800 600 0.3 2.5e-05 4 ] result: 12411.2 observed 12411.2 -[-0.000239526 -3.8053 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000294075 -3.71413 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0129062 4.25239 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0096217 -3.83826 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0119085 3.66862 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0114028 3.25696 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634443 -0.137021 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0133787 1.94199 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0148029 2.29333 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999712 -3.13244 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00997586 -3.37984 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00398442 -1.29767 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00473371 -0.955717 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0164817 2.73123 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181128 3.156 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0194328 3.5218 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932879 -3.99133 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891152 -4.12089 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00843313 -4.20378 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712674 0.314145 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0121895 4.16423 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00511943 -1.82833 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00253653 -3.09818 614 613 0.1 1 4287 5738 0.4 1.378 0.174 8.3 1 300 70000 1.8727e+07 1.8727e+07 1800 600 0.3 2.5e-05 4 ] result: 12411.2 observed 12411.2 -[-0.00902885 -4.09199 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985959 -3.61773 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0112855 3.1485 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00552972 -0.567007 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00961513 3.0719 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000462248 2.06372 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00422659 1.5806 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00617307 1.31034 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00158056 -4.24935 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000224375 -4.24047 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00396578 -4.21422 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0118203 1.58854 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.010695 3.83064 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0111552 3.71529 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00474927 0.948478 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00550844 0.578879 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00600078 0.324679 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00693205 -0.19454 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00383204 -4.20898 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.000900445 -3.98411 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00851264 -1.24836 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00957381 -2.24878 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00973585 -2.4703 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0105863 1.17471 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00782511 0.759179 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00248677 2.37298 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00798926 2.98563 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0171907 3.86998 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0159496 4.0065 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.018439 3.7753 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00480858 -2.17296 614 613 0.1 1 4287 5738 0.4 1.378 0.174 8.3 1 300 70000 1.8727e+07 1.8727e+07 1800 600 0.3 2.5e-05 4 ] result: 12411.2 observed 12411.2 -[-2.09022e-05 -3.80428 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-8.23502e-05 -3.71109 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0124645 4.24954 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0109462 4.08978 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00962203 -3.83799 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0117369 3.67075 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0113263 3.26095 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634474 -0.13682 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0131731 1.942 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.014643 2.29428 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00999694 -3.13015 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0099758 -3.37999 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00398239 -1.29857 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00473461 -0.955289 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0163904 2.73143 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181032 3.15611 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0195062 3.52173 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932861 -3.99137 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891178 -4.12082 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00843233 -4.20388 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712702 0.314349 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0118407 4.15719 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00496552 -1.82744 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00260561 -3.09576 614 613 0.1 1 4287 5738 0.4 1.378 0.174 8.3 1 300 70000 1.8727e+07 1.8727e+07 1800 600 0.3 2.5e-05 4 ] result: 12411.2 observed 12411.2 -[-0.00902186 -4.09385 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985943 -3.61787 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0113551 3.14872 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00553008 -0.566808 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00966745 3.07182 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000598993 2.06122 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00442089 1.57924 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00640223 1.30767 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00157494 -4.24945 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000231579 -4.24034 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00368831 -4.21486 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0115782 1.58836 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0104774 3.83031 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0109716 3.71689 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00474971 0.948268 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00550162 0.58233 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00599334 0.328641 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00749625 -0.539701 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00650905 0.048435 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00224062 -4.15436 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00131806 -4.06566 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00183239 -4.08005 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00850773 -1.24447 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00972237 -2.44988 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00987394 -2.71099 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00994849 -2.89508 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0103135 1.17501 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00782491 0.759092 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00241248 2.37317 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00800518 2.9825 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0170256 3.87028 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0157253 4.01276 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0183022 3.77456 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00467001 -2.18142 614 613 0.1 1 4287 5738 0.4 1.378 0.174 8.3 1 300 70000 1.8727e+07 1.8727e+07 1800 600 0.3 2.5e-05 4 ] result: 12411.2 observed 12411.2 -[0.000191976 -3.80391 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000161834 -3.70336 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0107361 4.09759 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00962234 -3.83773 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0115641 3.67277 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0112477 3.26466 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634505 -0.136625 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0129674 1.94202 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0144834 2.29519 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00995544 -2.91742 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00997576 -3.38017 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00398014 -1.29957 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00473543 -0.954896 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0162992 2.73162 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0180936 3.15622 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0195796 3.52166 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932845 -3.9914 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891204 -4.12074 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00843166 -4.20397 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712727 0.314542 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0115499 4.15678 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00481127 -1.82659 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00267679 -3.09273 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0121756 4.24986 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0148343 4.0776 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00901523 -4.0956 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985928 -3.61811 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0114242 3.14883 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00553067 -0.566498 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0097197 3.07173 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00073531 2.0589 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00461471 1.57804 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00663081 1.30523 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00156965 -4.24955 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000238575 -4.24021 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000784788 -4.21433 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0113362 1.5882 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0102604 3.83005 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0107871 3.7184 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00474945 0.948389 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00549531 0.585522 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00598526 0.33294 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0066074 -0.0068844 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00194956 -4.15417 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00109082 -4.06861 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00158508 -4.08182 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00850214 -1.24007 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00984678 -2.65681 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0093442 -1.98643 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00974513 -2.4844 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00994724 -2.89122 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00975655 1.17529 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00782472 0.759014 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00233872 2.37344 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00802116 2.97952 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0168603 3.87063 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0155133 4.01442 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0181656 3.77386 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00453616 -2.18908 614 613 0.1 1 4287 5738 0.4 1.378 0.174 8.3 1 300 70000 1.8727e+07 1.8727e+07 1800 600 0.3 2.5e-05 4 ] result: 12411.2 observed 12411.2 -[0.000389479 -3.80541 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000397258 -3.69646 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0105133 4.10403 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00962263 -3.83757 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0113901 3.67466 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.011167 3.26813 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634534 -0.136435 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0127618 1.94204 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0143238 2.29606 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00995396 -2.91257 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00997638 -3.37785 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00397783 -1.30059 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00473619 -0.954534 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.016208 2.73183 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.018084 3.15633 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0196529 3.52159 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932828 -3.99144 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00891232 -4.12067 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00322048 -4.20417 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00712751 0.314725 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0112587 4.15628 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00465661 -1.82582 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00274728 -3.08965 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0118671 4.2497 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0145712 4.08672 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00900892 -4.09725 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985913 -3.61835 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.011493 3.1489 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00553133 -0.566143 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00533516 3.07174 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000871231 2.05675 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00480809 1.57697 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00685882 1.303 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00156463 -4.24965 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000244903 -4.2401 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00104013 -4.21391 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0110941 1.58804 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0100438 3.82988 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0106018 3.71983 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00474887 0.948658 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00548943 0.5885 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00655259 0.023714 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00745897 -0.51597 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00608773 0.278863 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00166148 -4.15413 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000860946 -4.07143 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0013331 -4.08329 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00849323 -1.23311 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00982781 -2.62148 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00935889 -2.00196 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00974487 -2.48399 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00994371 -2.8806 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00840983 1.17557 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00782455 0.758943 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0047241 2.97552 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00306954 2.88907 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0166949 3.87098 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0153035 4.01525 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0180292 3.7732 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00440639 -2.19607 614 613 0.1 1 4287 5738 0.4 1.378 0.174 8.3 1 300 70000 1.8727e+07 1.8727e+07 1800 600 0.3 2.5e-05 4 ] result: 12411.2 observed 12411.2 -[0.000586492 -3.80704 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000625241 -3.69027 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0102844 4.10987 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0096229 -3.83744 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.011215 3.67641 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0110846 3.27138 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634563 -0.136249 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0125561 1.94208 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0141644 2.29689 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.0099527 -2.9085 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00997696 -3.37566 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00397552 -1.30162 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00473688 -0.9542 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0156151 2.73205 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0147633 3.15662 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0160533 3.52174 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932813 -3.99151 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0089126 -4.12059 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00305325 -4.20424 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00712774 0.314897 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0109673 4.15576 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00450157 -1.82511 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00281612 -3.08673 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0115577 4.24951 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0143088 4.09429 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000910929 2.37521 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00900293 -4.0988 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985899 -3.61857 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0115618 3.14894 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00553203 -0.565775 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00175646 3.07168 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0017628 2.05477 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00500106 1.57602 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00708631 1.30096 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00155989 -4.24974 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000250666 -4.24 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00105043 -4.21353 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.010852 1.5879 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0104225 3.72302 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00474816 0.948988 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00550052 0.582924 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00655382 0.0230606 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00743206 -0.498907 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00615118 0.244871 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00137459 -4.15415 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.000627607 -4.07403 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00107816 -4.08456 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00849081 -1.23118 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00937354 -2.01758 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0097446 -2.48355 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00994042 -2.87098 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00841009 1.17583 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00782438 0.758874 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00110893 2.9746 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.000536769 2.88637 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0165295 3.87132 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.015095 4.01563 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0178931 3.77257 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000783846 -3.80869 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.000846919 -3.68467 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0100486 4.11502 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00962316 -3.83732 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.0110389 3.67803 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0079548 3.27453 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00634592 -0.136067 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0123505 1.94212 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0140051 2.29769 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00995163 -2.90506 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0099775 -3.37359 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00397326 -1.30262 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00473753 -0.953891 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00988033 2.73245 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00999822 3.1569 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00992388 3.52196 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00932797 -3.99164 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.0089129 -4.12051 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00305229 -4.20429 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00712795 0.315061 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0106856 4.15641 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0106745 4.10072 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00432525 -1.83081 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00286691 -3.08763 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00469794 -2.36512 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.011249 4.24934 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.0140471 4.10067 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[-0.00253629 2.37361 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00429585 -2.1979 614 613 0.1 1 4287 5738 0.4 1.378 0.174 8.3 1 300 70000 1.8727e+07 1.8727e+07 1800 600 0.3 2.5e-05 4 ] result: 12411.2 observed 12411.2 -[-0.00899722 -4.10028 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[-0.00985886 -3.61879 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00305346 3.14919 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.3 observed 12412.3 -[0.00553272 -0.565407 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 -[0.00981718 3.82731 563 522 0.05 1 6099 7273 0.4 0.764 0.352 8.3 1 300 70000 1.8169e+07 1.8167e+07 1800 600 0.3 2.5e-05 4 ] result: 12412.2 observed 12412.2 diff --git a/tests/runANN/run.bash b/tests/runANN/run.bash index c3304675..c3767efd 100755 --- a/tests/runANN/run.bash +++ b/tests/runANN/run.bash @@ -1,11 +1,76 @@ -../../bin/ANN_test Rothermel.ffann modelrun.csv print | grep result > result.txt -if ! diff result.txt result.txt.ref; then - echo "ANN results differs from reference." +#!/bin/bash +# Checks that the trained network in Rothermel.ffann still reproduces the +# propagation model it was fitted to. +# +# This used to diff the per-input predictions against result.txt.ref, a file +# that is not in the repository, so the test failed on line 2 every time it +# ran. A plain rename of the committed result.txt would not have fixed it: +# regenerating that output changes a handful of lines in the last digit +# (12412.1 against 12412.2), so an exact diff is red on any machine but the +# one that produced the file. +# +# ANN_test already computes the root mean squared error between what the +# network predicts and what the CSV says the model produced, so that is what +# is checked here. It needs no reference file, it does not care about the last +# digit, and it fails loudly if the network stops approximating the model. +# +# For a per-input dump, add `print`: +# ../../bin/ANN_test Rothermel.ffann modelrun.csv print + +set -e + +ANN_TEST=../../bin/ANN_test +NETWORK=Rothermel.ffann +INPUTS=modelrun.csv + +# Calibration, measured on this fixture: +# +# trained network 0.0235 +# predicting the mean of every output 0.0966 +# normalisation weights scaled by 1% 17817 +# +# So the tolerance has to sit between the first two, or a network that ignores +# its inputs entirely would pass. 0.05 leaves the real network a factor of two +# of headroom and still fails a constant predictor. Anything that actually +# breaks the arithmetic lands orders of magnitude away and is not close. +# +# Note the fixture is weak: the expected outputs take four distinct values +# spanning 1.1 in 12412. That is why the gap between a working network and a +# constant one is so narrow, and it is worth replacing with a set of inputs +# that produce a real spread of rates of spread. +MAX_RMSE=0.05 + +if [ ! -x "$ANN_TEST" ]; then + echo "ANN_test not found at $ANN_TEST. Build with -DFOREFIRE_BUILD_TOOLS=ON." exit 1 fi -# Basic sanity checks on generated output -# Check that esult.txt exists and is not empty -if [ ! -s result.txt ]; then - echo "result.txt is empty or missing." +for required in "$NETWORK" "$INPUTS"; do + if [ ! -s "$required" ]; then + echo "missing input file: $required" + exit 1 + fi +done + +output=$("$ANN_TEST" "$NETWORK" "$INPUTS") +echo "$output" + +rmse=$(printf '%s\n' "$output" | sed -n 's/^Total Root Mean Squared Error: //p') +processed=$(printf '%s\n' "$output" | sed -n 's/^Time taken for processing \([0-9]*\) inputs.*/\1/p') + +if [ -z "$rmse" ]; then + echo "ANN_test printed no RMSE; it did not run to completion." exit 1 -fi \ No newline at end of file +fi + +if [ -z "$processed" ] || [ "$processed" -eq 0 ]; then + echo "ANN_test processed no inputs." + exit 1 +fi + +if ! awk -v got="$rmse" -v max="$MAX_RMSE" 'BEGIN { exit !(got <= max) }'; then + echo "ANN RMSE $rmse exceeds the tolerance of $max." + echo "The network no longer reproduces the propagation model it was fitted to." + exit 1 +fi + +echo "ANN test passed: $processed inputs, RMSE $rmse (tolerance $MAX_RMSE)." From f9b20ac9494df799e3fe833f84b11198a189c379 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 23:20:26 +0200 Subject: [PATCH 22/63] docs: add a CHANGELOG The repository has seven tags and six GitHub releases, and no file that tells a user what changed between them. `pip install forefire` makes that worse: someone pinning a version has the release notes on GitHub and nothing in the tree. CHANGELOG.md reconstructs every release from the release notes and the commit history. Claims that could be checked were checked against the tags: listenHTTP and RothermelAndrews2018 are absent at v1.2 and present at v2.0, the geojson dump mode is absent at v1.1.10. [Unreleased] lists only what is on this branch, #154 and #155. The dev branch carries a longer list in its own copy, which supersedes this one when dev is merged. CONTRIBUTING.md now asks for an entry, so the file stays current instead of being reconstructed again in a year. Its link to a testing page pointed at a Read the Docs URL that has never existed; it points at TESTING.md, which nothing in the repository linked to. (cherry picked from commit cc7e1789c66432aec56673993c3b6e198e0525a0) --- CHANGELOG.md | 137 ++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 11 ++-- README.md | 2 + 3 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a5644e65 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,137 @@ +# Changelog + +All notable changes to ForeFire are recorded here. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +Version numbers are the tags in this repository and the values reported by +`forefire -v`; they are also what `pip install forefire==` resolves. + +Entries below v2.5.0 were reconstructed from the release notes and the commit +history, so they summarise each release rather than list every change. The +`Full Changelog` link on each version gives the complete commit range. + +## [Unreleased] + +Merged since v2.5.0. Work still on the `dev` branch is listed in that branch's +copy of this file. + +### Changed + +- Linux wheels are built against a NetCDF without DAP and HDF4, roughly halving + the wheel. ([#154]) +- Free-threaded (`cp314t`) wheels are no longer published, because the core is + not yet thread-safe; CPython 3.14 is declared supported. ([#155]) + +## [v2.5.0] — 2026-08-11 + +### Added + +- **Pip-installable wheels** for Linux (x86_64, aarch64) and macOS (Apple + Silicon and Intel), CPython 3.9 and newer. `pip install forefire` gives both + the `forefire` command-line interpreter and the `pyforefire` module, with + NetCDF bundled inside the wheel. Wheels are built without MPI and without + `-march=native`. ([#151]) + +### Changed + +- The Dockerfile is a multi-stage build, and only `libforefireL` is built in + the builder stage. ([#147]) +- The `install-forefire.sh` build is driven by an option-based `CMakeLists.txt` + (`FOREFIRE_ENABLE_MPI`, `FOREFIRE_NATIVE_ARCH`, `FOREFIRE_BUILD_PYTHON`, + `FOREFIRE_STATIC_CORE`, `FOREFIRE_BUILD_TOOLS`, `FOREFIRE_CHECK_LFS`). + +### Fixed + +- A double free in `FireFront` cleanup. ([#145]) +- `CMakeLists.txt` was matched by `.gitignore`, so it was missing from the + source distribution. ([#152]) +- Bugs in `tests/run.bash`, including a `runANN` typo and an unreachable + `./clean.bash`. ([#146]) + +**Full Changelog**: + +## [v2.4.2] — 2025-11-27 + +- Simplified the installation process, following review comments on the JOSS + submission, and corrected the installation instructions. +- Aligned the coupling with + [PACK-MNH-V5-7-2](https://src.koda.cnrs.fr/mesonh/mesonh-code/-/releases/PACK-MNH-V5-7-2). +- Aligned the repository with the accepted JOSS paper + ([10.21105/joss.08680](https://doi.org/10.21105/joss.08680)). + +**Full Changelog**: + +## [v2.1.122] — 2025-09-16 + +Tagged, but never published as a GitHub release. + +- Docker images published to the GitHub container registry, and tested in CI. +- Python bindings reworked, with the example from [#103] applied. +- macOS CI runs the test script. + +**Full Changelog**: + +## [v2.0] — 2025-06-05 + +The V2 release: the HTTP interface, and alignment with Meso-NH V4.7.2. + +### Added + +- The built-in **HTTP command server and web UI** (`listenHTTP[]`, or + `forefire -l`), serving a map view of the simulation. +- The **Read the Docs documentation site**, built with Sphinx, Breathe and + Doxygen. +- `install-forefire.sh`, with `-y` to add `forefire` to `PATH` and set + `FOREFIREHOME`. +- A working Dockerfile, and GitHub Actions for Linux and macOS. +- The `RothermelAndrews2018` propagation model. ([#34]) + +### Changed + +- CMake replaces SCons throughout; references to SCons were removed. + +### Fixed + +- The `-lnetcdf_c++4` link failure. ([#26]) +- A division by zero in `setArrivalTime`/`getArrivalTime`. ([#36]) + +**Full Changelog**: + +## [v1.2] — 2024-01-16 + +### Added + +- The `geojson` dump mode. + +## [v1.1.10] — 2022-10-25 + +### Changed + +- CMake became the default build system. ([#9]) + +## [v1.1.0] — 2022-09-28 + +Tagged before the sources were moved into `src/`, so that this point in the +repository stays easy to return to. + +[Unreleased]: https://github.com/forefireAPI/forefire/compare/v2.5.0...dev +[v2.5.0]: https://github.com/forefireAPI/forefire/releases/tag/v2.5.0 +[v2.4.2]: https://github.com/forefireAPI/forefire/releases/tag/v2.4.2 +[v2.1.122]: https://github.com/forefireAPI/forefire/releases/tag/v2.1.122 +[v2.0]: https://github.com/forefireAPI/forefire/releases/tag/v2.0 +[v1.2]: https://github.com/forefireAPI/forefire/releases/tag/v1.2 +[v1.1.10]: https://github.com/forefireAPI/forefire/releases/tag/v1.1.10 +[v1.1.0]: https://github.com/forefireAPI/forefire/releases/tag/v1.1.0 + +[#9]: https://github.com/forefireAPI/forefire/issues/9 +[#26]: https://github.com/forefireAPI/forefire/pull/26 +[#34]: https://github.com/forefireAPI/forefire/pull/34 +[#36]: https://github.com/forefireAPI/forefire/pull/36 +[#103]: https://github.com/forefireAPI/forefire/issues/103 +[#145]: https://github.com/forefireAPI/forefire/pull/145 +[#146]: https://github.com/forefireAPI/forefire/pull/146 +[#147]: https://github.com/forefireAPI/forefire/pull/147 +[#151]: https://github.com/forefireAPI/forefire/pull/151 +[#152]: https://github.com/forefireAPI/forefire/pull/152 +[#154]: https://github.com/forefireAPI/forefire/pull/154 +[#155]: https://github.com/forefireAPI/forefire/pull/155 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3becf3bc..0c8a0305 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,25 +64,28 @@ We welcome code contributions, from bug fixes to new features. Implement your code changes, following existing coding style and conventions where possible. 6. **Add Tests (if applicable):** - For new features or significant bug fixes, please add corresponding tests or update existing ones. See the [Testing Documentation](https://forefire.readthedocs.io/en/latest/developer_guide/testing.html) *(Placeholder: Link to testing section once created)* for details on how to run tests. Ensure all tests pass locally. + For new features or significant bug fixes, please add corresponding tests or update existing ones. See [TESTING.md](TESTING.md) for what each suite covers and how to run it. Ensure all tests pass locally. ```bash # Example command to run tests (adjust as needed) cd tests && bash run.bash ``` -7. **Commit Changes:** +7. **Record the Change:** + + Add an entry under `## [Unreleased]` in [CHANGELOG.md](CHANGELOG.md), in the section that fits (`Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`). Write it for someone upgrading: what changed for them, not what you edited. Skip this for changes with no user-visible effect, such as a typo fix or an internal refactor. +8. **Commit Changes:** Commit your changes with clear and concise commit messages. ```bash git add . git commit -m "feat: Implement new flux model calculation" ``` -8. **Push to Your Fork:** +9. **Push to Your Fork:** Push your branch to your GitHub fork. ```bash git push origin my-feature-branch ``` -9. **Open a Pull Request (PR):** +10. **Open a Pull Request (PR):** Go to the `forefireAPI/forefire` repository on GitHub and open a Pull Request from your branch to the `dev` branch (or `master` if that's the target). * Provide a clear description of the changes in the PR. diff --git a/README.md b/README.md index 229a53e3..0e492164 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ - 📚 **Full Documentation:** [forefire.readthedocs.io](https://forefire.readthedocs.io/en/latest/) - 🚀 **Live Demo:** [forefire.univ-corse.fr/sim](http://forefire.univ-corse.fr/sim) - 🌍 **Website:** [forefire.univ-corse.fr](https://forefire.univ-corse.fr/) +- 📝 **Changelog:** [CHANGELOG.md](CHANGELOG.md) — what each release changed +- 🧪 **Testing:** [TESTING.md](TESTING.md) — how to run each test suite ## Features From b8c562f2f2cd8737c7eec6d031048095ffb58de4 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Wed, 12 Aug 2026 23:21:24 +0200 Subject: [PATCH 23/63] docs: record the dev-only work, and the options it added The CHANGELOG landed on master carries only #154 and #155 under [Unreleased], because that is all master has. This adds the ten pull requests merged into dev since: the unit suite, the moisture invariants, the sanitizer job, the HTTP characterisation tests, the threading reproduction, and the shared-state and double-free fixes. Two things went stale on dev while that work landed, both of them ours. The README's CMake option table stopped at six options; #156 and #180 added FOREFIRE_BUILD_TESTS, FOREFIRE_ENABLE_WARNINGS, FOREFIRE_WARNINGS_AS_ERRORS and FOREFIRE_SANITIZE. And CONTRIBUTING told contributors to run `cd tests && bash run.bash`, which does not reach the C++ unit tests at all; ctest does. --- CHANGELOG.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++-- CONTRIBUTING.md | 8 +++++-- README.md | 8 ++++++- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5644e65..6d77f867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,52 @@ history, so they summarise each release rather than list every change. The ## [Unreleased] -Merged since v2.5.0. Work still on the `dev` branch is listed in that branch's -copy of this file. +Merged since v2.5.0, not yet released. + +### Added + +- C++ unit tests covering every propagation and flux model, built by default + and registered with CTest. They exercise the models one call at a time, + which the `runff` regression test cannot reach. ([#156]) +- A dead fuel moisture invariant suite (`tests/python/test_moisture_invariants.py`) + and the `invariants.yml` workflow that runs it. Unlike `runff` it holds no + reference data: every assertion follows from the published spread equations, + so it stays valid across recalibration. ([#158]) +- A blocking AddressSanitizer job, and `-DFOREFIRE_SANITIZE=` to build + with `-fsanitize=` on the compile line, the executables and the shared + library. ([#180]) +- Characterisation tests pinning the HTTP command server's current behaviour. + ([#174]) +- A concurrency stress test for free-threaded CPython + (`tests/python/test_threading.py`). It does not pass yet: it is the failing + test for the shared-state work in [#175]. ([#176]) +- `-Wall -Wextra` on ForeFire's own sources, with `FOREFIRE_ENABLE_WARNINGS` + and `FOREFIRE_WARNINGS_AS_ERRORS` to control them. NetCDF's headers are + included as system headers so their warnings do not appear. ([#156]) +- `FOREFIRE_BUILD_TESTS` (default on outside wheel builds) to build the unit + tests. ([#156]) +- `TESTING.md` now documents every suite, how to run it, and which ones CI + validates. + +### Fixed + +- Rate of spread stayed finite and decreasing at high dead fuel moisture, and + `DataBroker` no longer serves `moisture` through the five-slot getter, which + returned a neighbouring property. ([#158]) +- The model properties array was freed twice on destruction. ([#157]) +- `~ForeFireModel` freed an uninitialised pointer when construction had not + reached the allocation. ([#156]) +- `runANN` had failed on its second line since it was committed: it diffed + against `result.txt.ref`, a file that is not in the repository. It now checks + the root mean squared error that `ANN_test` already computes, which does not + depend on the last digit of a machine-specific reference, and it runs in CI. + ([#183]) +- The `ForeFireAtom` instance counter is now atomic and the `SimulationParameters` + singleton is initialised safely, so two threads no longer race for ids or + construct the singleton twice. ([#177]) +- `StringRepresentation` kept its output buffer, current level and GeoJSON + cursor in file-scope globals shared by every instance. They are now members. + ([#178]) ### Changed @@ -135,3 +179,13 @@ repository stays easy to return to. [#152]: https://github.com/forefireAPI/forefire/pull/152 [#154]: https://github.com/forefireAPI/forefire/pull/154 [#155]: https://github.com/forefireAPI/forefire/pull/155 +[#156]: https://github.com/forefireAPI/forefire/pull/156 +[#157]: https://github.com/forefireAPI/forefire/pull/157 +[#158]: https://github.com/forefireAPI/forefire/pull/158 +[#174]: https://github.com/forefireAPI/forefire/pull/174 +[#175]: https://github.com/forefireAPI/forefire/issues/175 +[#176]: https://github.com/forefireAPI/forefire/pull/176 +[#177]: https://github.com/forefireAPI/forefire/pull/177 +[#178]: https://github.com/forefireAPI/forefire/pull/178 +[#180]: https://github.com/forefireAPI/forefire/pull/180 +[#183]: https://github.com/forefireAPI/forefire/pull/183 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c8a0305..48a18128 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,9 +64,13 @@ We welcome code contributions, from bug fixes to new features. Implement your code changes, following existing coding style and conventions where possible. 6. **Add Tests (if applicable):** - For new features or significant bug fixes, please add corresponding tests or update existing ones. See [TESTING.md](TESTING.md) for what each suite covers and how to run it. Ensure all tests pass locally. + For new features or significant bug fixes, please add corresponding tests or update existing ones. See [TESTING.md](TESTING.md) for what each suite covers, how to run it, and which ones CI validates. Ensure all tests pass locally. ```bash - # Example command to run tests (adjust as needed) + # The C++ unit tests + cmake -S . -B build && cmake --build build -j + ctest --test-dir build --output-on-failure + + # The script-driven suites (runff, runANN) cd tests && bash run.bash ``` 7. **Record the Change:** diff --git a/README.md b/README.md index 0e492164..b70952c1 100644 --- a/README.md +++ b/README.md @@ -159,9 +159,15 @@ The CMake build is option-driven. The defaults below are what a plain | `FOREFIRE_STATIC_CORE` | `OFF` | Build the core as a static library instead of `libforefireL`. | | `FOREFIRE_BUILD_TOOLS` | `ON` | Build the `ANN_test` helper executable. | | `FOREFIRE_CHECK_LFS` | `ON` | Run the Git LFS integrity check while configuring. | +| `FOREFIRE_BUILD_TESTS` | `ON` | Build the C++ unit tests and register them with CTest. | +| `FOREFIRE_ENABLE_WARNINGS` | `ON` | Compile ForeFire's own sources with `-Wall -Wextra`. | +| `FOREFIRE_WARNINGS_AS_ERRORS` | `OFF` | Fail the build on any compiler warning. Not yet usable repository-wide. | +| `FOREFIRE_SANITIZE` | `""` | Sanitizers to build with, passed to `-fsanitize=` (e.g. `address`). | Wheel builds (anything driven by `pip`) flip these to the portable defaults: -no MPI, no `-march=native`, static core, Python module on. +no MPI, no `-march=native`, static core, Python module on, no tools, no tests. + +`TESTING.md` covers the test suites and the sanitizer build in detail. ## Python Bindings ForeFire provides Python bindings for easier scripting and integration: From 96e359e81ddada50ed6c11b4f7e2bd0f2babbf99 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Thu, 13 Aug 2026 00:10:37 +0200 Subject: [PATCH 24/63] docs(rtd): document the pip install path The documentation site had no mention of pip, pypi or wheels -- `grep -rni 'pip\|pypi\|wheel' docs/source` returned nothing -- while the README's headline instruction is `pip install forefire`. Every reader arriving at the site was sent to build from source. - installation.rst leads with pip: platforms, what the wheel contains, and the three options wheels turn off (MPI, -march=native, ANN_test), then the source build as before. - The NetCDF prerequisite said 'Verify which one is actually required by the current CMake setup', leaving the reader to answer the documentation's own question. CMakeLists.txt looks for netcdf_c++4 / netcdf-cxx4 / netcdf-cxx and its failure message already lists the package per distribution; that table is now in the page, along with the note that libnetcdf-cxx-legacy-dev is a different API. - The FOREFIRE_* build options are documented, with their real defaults. - quickstart.rst offered Docker only, calling it 'the quickest way'. A pip route comes first now; the Docker walkthrough is unchanged. - conf.py hard-coded release = '2.0.0', so the site advertised 2.0.0 while the code was v2.5.0. It parses src/include/Version.h, as CMake and scikit-build-core do. Sphinx builds clean: the only warnings are the eleven doxygenclass lookups that need the Doxygen XML RTD generates in its pre_build step. --- docs/source/conf.py | 15 +- docs/source/getting_started/installation.rst | 268 ++++++++++++++----- docs/source/getting_started/quickstart.rst | 53 +++- docs/source/index.rst | 1 + 4 files changed, 271 insertions(+), 66 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 57574fa3..b87f841e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,10 +6,23 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +import pathlib +import re + project = 'ForeFire' copyright = '2014 - 2025, J-B Filippi' author = 'Filippi, Jean Baptiste' -release = '2.0.0' + +# Single source of truth for the version, the same file CMake and +# scikit-build-core read. Hard-coding it here left the site advertising 2.0.0 +# for three releases. +_version_header = pathlib.Path(__file__).resolve().parents[2] / 'src' / 'include' / 'Version.h' +_match = re.search(r'ff_version\s*=\s*"v?([0-9]+\.[0-9]+\.[0-9]+)"', + _version_header.read_text(encoding='utf-8')) +if not _match: + raise RuntimeError(f'could not parse ff_version from {_version_header}') +release = _match.group(1) +version = release # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/getting_started/installation.rst b/docs/source/getting_started/installation.rst index 0238a01d..ebe60466 100644 --- a/docs/source/getting_started/installation.rst +++ b/docs/source/getting_started/installation.rst @@ -1,32 +1,137 @@ -Installation: Build from Source -================================ +Installation +============ -This guide explains how to install ForeFire by compiling it directly on your Linux/Unix-like system. This gives you a native build, but requires managing dependencies. -There are two main ways to build from source: +There are three ways to install ForeFire. Which one you want depends on what +you intend to do with it: -- Option 1: Using the provided install script (easiest for Debian/Ubuntu) -- Option 2: Following manual steps (for all systems or finer control). +.. list-table:: + :header-rows: 1 + :widths: 20 45 35 -Prerequisites -------------- + * - Method + - Use it when + - Section + * - **pip** + - You want to run simulations or drive ForeFire from Python. + - :ref:`install-pip` + * - **Docker** + - You are on Windows, or you want the web console with no setup. + - :doc:`quickstart` + * - **From source** + - You need MPI coupling with Meso-NH, a CPU-tuned build, or you are + working on ForeFire itself. + - :ref:`install-source` + +.. _install-pip: + +Install with pip +---------------- + +This is the fastest route, and it needs no compiler, no CMake and no NetCDF +installation of your own — NetCDF is bundled inside the wheel. + +.. code-block:: bash + + pip install forefire + +Requirements +~~~~~~~~~~~~ + +- **CPython 3.9 to 3.14.** The free-threaded builds (``cp314t``) are + deliberately not published: the C++ core still keeps mutable global state, so + a wheel advertising free-threading would silently re-enable the GIL on + import. +- **Linux** on ``x86_64`` or ``aarch64`` (``manylinux_2_28`` or newer), or + **macOS 14+ on Apple Silicon**. + +There is no wheel for Intel macOS, musl-based Linux (Alpine), 32-bit targets, +Windows or PyPy. On those platforms pip falls back to the source distribution, +which needs the build prerequisites in :ref:`install-source`. Windows users are +better served by Docker — see :doc:`quickstart`. + +What you get +~~~~~~~~~~~~ + +Both interfaces, from the one package: + +.. code-block:: bash + + forefire -v + +.. code-block:: python + + import pyforefire as forefire -If you choose the **manual build steps** (Option 2), or if you simply want to understand what tools are needed, you must ensure the following are installed on your system: + ff = forefire.ForeFire() + ff.execute("FireDomain[sw=(0,0,0);ne=(10000,10000,0);t=0]") + ff.addLayer("propagation", "Iso", "propagationModel") + ff.execute("startFire[loc=(5000,5000,0.0)]") + ff.execute("step[dt=1000]") + print(ff.execute("print[]")) -- **C++ Compiler:** A modern C++ compiler (like `g++`). Package typically called `build-essential` or similar. -- **CMake:** Build system generator (`cmake`). -- **Make:** Build tool (`make`). -- **NetCDF Libraries:** ForeFire requires NetCDF support. The specific package needed is the C++ interface. +.. note:: - - On **Debian/Ubuntu**, the install script uses `libnetcdf-c++4-dev`. - - On other systems, find the equivalent package (e.g., `netcdf-cxx-devel`, `netcdf-cxx4`). - - *Note: Ensure this is the correct/intended library. Older docs might mention `libnetcdf-cxx-legacy-dev`. Verify which one is actually required by the current CMake setup.* + The distribution on PyPI is called ``forefire``; the importable module keeps + its historical name, ``pyforefire``. -Build Options -------------- +What you do not get +~~~~~~~~~~~~~~~~~~~ -**Option 1: Using the Install Script (Recommended for Debian/Ubuntu)** +Published wheels are built with three options turned off, so a few things are +only available in a source build: -The repository provides a convenience script (`install-forefire.sh`) that automates the process on Debian-based systems like Ubuntu. +- **MPI coupling.** Fire-atmosphere runs with Meso-NH need + ``FOREFIRE_ENABLE_MPI``, so they need a source build. +- **CPU tuning.** Wheels are built without ``-march=native``, so that they run + on any machine of the right architecture. A source build with + ``FOREFIRE_NATIVE_ARCH=ON`` will be faster on the machine that built it. +- **The** ``ANN_test`` **helper**, built by ``FOREFIRE_BUILD_TOOLS``. + +.. _install-source: + +Build from source +----------------- + +A native build, in exchange for managing the dependencies yourself. Two routes: +the install script (Debian/Ubuntu only), or manual steps (any Unix-like +system). + +Prerequisites +~~~~~~~~~~~~~ + +- **A C++ compiler**, such as ``g++``. On Debian/Ubuntu this comes with + ``build-essential``. +- **CMake** 3.15 or newer, and **Make**. +- **NetCDF — both the C library and the legacy C++4 API.** The C++4 API is a + separate package from the C library on every distribution, and it is the one + people usually miss. + + .. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - System + - Packages + * - Debian/Ubuntu + - ``apt install libnetcdf-dev libnetcdf-c++4-dev`` + * - Fedora/RHEL + - ``dnf install netcdf-devel netcdf-cxx4-devel`` + * - macOS (Homebrew) + - ``brew install netcdf netcdf-cxx`` + + The library is named ``netcdf_c++4`` everywhere except Homebrew, which calls + it ``netcdf-cxx4`` (``netcdf-cxx`` in older bottles); CMake looks for all + three. The older ``libnetcdf-cxx-legacy-dev`` package is a *different*, + pre-C++4 API and will not work. + + If NetCDF is installed somewhere CMake does not search, point at it with + ``-DNETCDF_HOME=/path/to/netcdf`` (and ``-DNETCDF_CXX_HOME=...`` if the C++ + API lives elsewhere). + +Option 1: the install script (Debian/Ubuntu) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``install-forefire.sh`` automates the process on Debian-based systems. 1. **Clone the repository:** @@ -39,27 +144,33 @@ The repository provides a convenience script (`install-forefire.sh`) that automa .. warning:: - This script requires `sudo` privileges to install system packages using `apt`. Review the script if you have concerns. + This script requires ``sudo`` privileges to install system packages using + ``apt``. Review the script if you have concerns. .. code-block:: bash sudo bash install-forefire.sh -**What the Install Script Does:** +**What the install script does:** -- **Updates Package Lists and Installs Dependencies:** Runs `apt-get update` and installs prerequisites listed above -- **Builds ForeFire:** using CMake and Make. -- **Reports Install Location:** Prints the location of the built binaries (usually `$PROJECT_ROOT/bin`). -- **(Optional) Updates PATH:** +- **Installs dependencies:** runs ``apt-get update`` and installs the + prerequisites listed above. +- **Builds ForeFire** using CMake and Make. +- **Reports the install location:** usually ``$PROJECT_ROOT/bin``. +- **(Optional) updates PATH:** - - Prompts the user if they want to add the ForeFire `bin` directory to their PATH permanently. - - If 'yes', it appends `export PATH=` and `export FOREFIREHOME=` lines to the user's `~/.bashrc` file. - - It tries to detect the correct user's home directory even when run with `sudo` (using `$SUDO_USER`). - - **Note:** This only modifies `.bashrc`. If you use a different shell (like `zsh` or `fish`), you will need to configure the PATH manually (see below). + - Prompts you before doing anything, and only if you agree appends + ``export PATH=`` and ``export FOREFIREHOME=`` lines to ``~/.bashrc``. + - It detects the invoking user's home directory even under ``sudo``, via + ``$SUDO_USER``. + - **Note:** this only modifies ``.bashrc``. For ``zsh`` or ``fish``, + configure the PATH manually (see below). -**Option 2: Manual Build Steps (All Linux/Unix-like Systems)** +Option 2: manual build (any Linux/Unix-like system) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use this method if you are not on Debian/Ubuntu, prefer manual control, or don't want to use the install script. +Use this if you are not on Debian/Ubuntu, prefer manual control, or do not +want to run the install script. 1. **Clone the repository:** @@ -68,55 +179,88 @@ Use this method if you are not on Debian/Ubuntu, prefer manual control, or don't git clone https://github.com/forefireAPI/forefire.git cd forefire -2. **Install Prerequisites Manually:** +2. **Install the prerequisites** for your system, from the table above. + +3. **Configure and build:** - Use your system's package manager to install `cmake`, `make`, a C++ compiler (`build-essential` or equivalent), and the required NetCDF C++ development library (e.g., `libnetcdf-c++4-dev`, `netcdf-cxx-devel`, etc.). - Example for Debian/Ubuntu (if not using the script): - .. code-block:: bash - sudo apt update - sudo apt install build-essential cmake libnetcdf-c++4-dev # Verify package name! + cmake -S . -B build + cmake --build build -j -3. **Create a build directory and run CMake & Make:** + The executable lands in ``bin/forefire``. Check it with: .. code-block:: bash - mkdir build - cd build - cmake .. - make + ./bin/forefire -v -The main executable `forefire` will be located at `../bin/forefire` (relative to the `build` directory). Check installation with - - .. code-block:: bash +Build options +~~~~~~~~~~~~~ - cd .. # Go back to the root of the repository - ./bin/forefire -v +The build is driven by ``FOREFIRE_*`` CMake options; pass them at configure +time, for example ``cmake -S . -B build -DFOREFIRE_ENABLE_MPI=OFF``. -4. **Making ForeFire Executable System-Wide (Manual PATH setup)** +.. list-table:: + :header-rows: 1 + :widths: 30 15 55 - If you built from source (manually or via the script but declined the automatic PATH update, or use a shell other than bash), the `forefire` executable is in the `bin` directory within the repository. To run it easily from any location, add this directory to your system's PATH environment variable. + * - Option + - Default + - Effect + * - ``FOREFIRE_ENABLE_MPI`` + - ON + - Fire-atmosphere coupling with Meso-NH, when MPI is found. Replaces the + compiler with the MPI wrapper. + * - ``FOREFIRE_NATIVE_ARCH`` + - ON + - ``-march=native``. Turn it off for a binary you intend to move to + another machine. + * - ``FOREFIRE_BUILD_PYTHON`` + - OFF + - Build the ``pyforefire`` extension module. + * - ``FOREFIRE_STATIC_CORE`` + - OFF + - Link the core statically. + * - ``FOREFIRE_BUILD_TOOLS`` + - ON + - Build the ``ANN_test`` helper, needed by ``tests/runANN``. + * - ``FOREFIRE_CHECK_LFS`` + - ON + - Fail early if the Git LFS test fixtures were not pulled. - **For the current terminal session:** +Wheel builds flip all six: MPI, native-arch, tools and the LFS check off, +Python and the static core on. That is what makes a wheel run on a machine +other than the one that built it. - .. code-block:: bash +Making ForeFire available system-wide +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Execute this from the root of the forefire repository - export PATH=$PATH:`pwd`/bin +After a source build, ``forefire`` lives in the repository's ``bin`` +directory. To run it from anywhere, add that directory to your PATH. - **Permanently:** +**For the current terminal session:** - Add the following line to your shell's configuration file (e.g., `~/.bashrc`, `~/.zshrc`, `~/.profile`, or `~/.config/fish/config.fish`). Replace `/path/to/forefire` with the actual absolute path to the cloned repository. +.. code-block:: bash - .. code-block:: bash + # Execute this from the root of the forefire repository + export PATH=$PATH:`pwd`/bin - export PATH="/path/to/forefire/bin:$PATH" +**Permanently:** - *Optional:* The install script also sets `export FOREFIREHOME="/path/to/forefire"`. You may want to add this line as well, as some scripts or components might potentially use it. +Add the following to your shell's configuration file (``~/.bashrc``, +``~/.zshrc``, ``~/.profile``, or ``~/.config/fish/config.fish``), replacing +``/path/to/forefire`` with the absolute path to the cloned repository. - .. code-block:: bash +.. code-block:: bash + + export PATH="/path/to/forefire/bin:$PATH" + +*Optional:* the install script also sets ``FOREFIREHOME``, which some scripts +and components use to locate the repository. + +.. code-block:: bash - export FOREFIREHOME="/path/to/forefire" + export FOREFIREHOME="/path/to/forefire" - After editing your configuration file, either restart your terminal or reload the configuration (e.g., `source ~/.bashrc`). \ No newline at end of file +Then restart your terminal or reload the configuration, for example with +``source ~/.bashrc``. diff --git a/docs/source/getting_started/quickstart.rst b/docs/source/getting_started/quickstart.rst index 2a575b13..e4b5dfcd 100644 --- a/docs/source/getting_started/quickstart.rst +++ b/docs/source/getting_started/quickstart.rst @@ -1,15 +1,62 @@ Quick Start =========== -This guide shows the quickest way to get the standard **ForeFire example simulation running** using its interactive web console, powered by Docker. This method bundles all dependencies, so you don't need to install them on your host system, and is the only way to run ForeFire on Windows. +Your first simulation, two ways: from a pip install in about a minute, or in +Docker with the interactive web console. + +.. _quickstart-pip: + +The one-minute version, with pip +-------------------------------- + +On Linux or macOS on Apple Silicon, nothing needs to be compiled and no +dependencies need to be installed on your system: + +.. code-block:: bash + + pip install forefire + +Then run a simulation from Python: + +.. code-block:: python + + import pyforefire as forefire + + ff = forefire.ForeFire() + ff.execute("FireDomain[sw=(0,0,0);ne=(10000,10000,0);t=0]") + ff.addLayer("propagation", "Iso", "propagationModel") + ff.execute("startFire[loc=(5000,5000,0.0)]") + ff.execute("step[dt=1000]") + print(ff.execute("print[]")) + +``print[]`` returns the state of the simulation as text — one ``FireNode`` +entry per node of the front, each with its location, velocity and time. The +same commands work in the interactive interpreter, which the same install +provides: + +.. code-block:: bash + + forefire + +See :doc:`installation` for which platforms have wheels, and +:doc:`/user_guide/forefire_script` for what these commands mean. + +.. _quickstart-docker: + +The full example, with Docker +----------------------------- + +This runs the standard **ForeFire example simulation** in its interactive web +console. It bundles all dependencies, so nothing is installed on your host +system, and it is the only way to run ForeFire on Windows. Prerequisites -------------- +~~~~~~~~~~~~~ - Docker installed and running on your system. - Git installed (for cloning the repository). Steps ------ +~~~~~ 1. **Clone the ForeFire repository:** Open your terminal and run: diff --git a/docs/source/index.rst b/docs/source/index.rst index 33ba48c6..84ac7a8c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,6 +6,7 @@ Welcome to the official documentation for ForeFire — the open-source wildfire **Key Links:** - `GitHub Repository `_ +- `ForeFire on PyPI `_ — ``pip install forefire`` - `Online Demo Simulator `_ .. toctree:: From c4ee9c57fbc664b78020072a2125c88834abc88c Mon Sep 17 00:00:00 2001 From: HugoFara Date: Thu, 13 Aug 2026 00:12:44 +0200 Subject: [PATCH 25/63] docs(tests): correct the test READMEs against the tree Nearly every specific claim in tests/README.md was wrong: - `idealized_wind.py` and `rothermel.fann` do not exist; the files are `idealizedwind.py` and `Rothermel.ffann`. - runANN was listed as needing `tensorflow` (or `torch`). It needs neither. It runs `bin/ANN_test`, built from tools/runANN/ANNTest.cpp, and ForeFire reads the .ffann network itself. - runANN was described as comparing against reference outputs. It diffs against result.txt.ref, which is not in the repository, so the suite fails on its second line every time -- now stated, with a pointer to #163. - percolation.py runs four fires, not three: one per entry in k_coeffs. - idealizedwind.py writes no NetCDF, only 360wind.png, and by way of ForeFire's plot[] command rather than matplotlib. - runff has two entry points that do different things. run.bash runs three scenarios and checks the artefacts exist; ff-run.bash runs two and compares KML and NetCDF against references. CI calls the second, so only the second can catch physics drift. The old text described neither accurately. tests/python/README.md documented only farsite_flat.py, the one script that cannot run as checked out, and did not say so. It now covers all four files, keeps the download URL for flatland.lcp, and marks test_wheel.py as belonging to cibuildwheel rather than to this suite. --- tests/README.md | 150 ++++++++++++++++++++++++++--------------- tests/python/README.md | 49 ++++++++++++-- 2 files changed, 138 insertions(+), 61 deletions(-) diff --git a/tests/README.md b/tests/README.md index 31844cd9..34ef8285 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,93 +1,131 @@ +# ForeFire test suite -# *FireFront* Test Suite +Five sets of tests, one per directory, covering the different interfaces and +use-cases of ForeFire. -This directory contains **five** sets of tests that showcase the different interfaces and use-cases of *ForeFire*. +| Directory | What it covers | Needs | +| --- | --- | --- | +| `mnh_ideal` | ForeFire / **Meso-NH** coupling on an idealised atmospheric case | `SRC_MESONH` set; Meso-NH compiled with the ForeFire library in its `exe` directory | +| `mnh_real_nested` | ForeFire / **Meso-NH** coupling on a real nested case | Same as above | +| `python` | The Python bindings, through two example simulations | `PYTHONEXE` set to a Python interpreter that can `import pyforefire` | +| `runANN` | The built-in feed-forward network evaluator, on a network fitted to Rothermel | `bin/ANN_test`, built by default (`-DFOREFIRE_BUILD_TOOLS=ON`) | +| `runff` | The command-line interpreter: run a case, save and reload state, export KML and GeoJSON | ForeFire only | -| Folder / script | Purpose of the test | Specific dependencies | -| --------------- | ------------------ | --------------------- | -| `mnh_ideal` | *ForeFire* / **Meso-NH** coupling on an idealised atmospheric case | `SRC_MESONH` must be set; Meso-NH compiled with the *ForeFire* library placed in its `exe` directory | -| `mnh_real_nested` | *ForeFire* / **Meso-NH** coupling on a real nested case | Same requirements as above | -| `python` | Usage examples via the Python API:
• `idealized_wind.py` (360° rotating wind)
• `percolation.py` (propagation through fuels of random density) | Python bindings must be set and PYTHONEXE set to the python eneble binary | -| `runANN` | Activation of a serialised ANN graph (`rothermel.fann`) that reproduces the Rothermel model | `tensorflow` (or `torch` if a PyTorch graph is supplied) | -| `runff` | Using the *ForeFire* command-line interpreter to:
1. launch a real simulation;
2. save/load a state;
3. convert outputs (FF, KML, GeoJSON) | *ForeFire* only | +`TESTING.md` at the repository root describes `runff` — the suite CI actually +gates on — in more detail. ---- +## Prerequisites -## 1. General prerequisites +* **ForeFire built**, with the binaries in `bin/`. -* **ForeFire** compiled and available in the `bin` dir. +* The test fixtures are stored in **Git LFS**. `git lfs pull` if the `.nc` + files look like short text stubs. -* *(optional)* **Meso-NH** – required only for the `mnh_*` tests. -```bash +* *(optional)* **Meso-NH**, for the `mnh_*` tests only: + + ```bash export SRC_MESONH=/path/to/your/mesonh -``` + ``` -* *(optional)* **Python bindings** – required for the `python` folder +* *(optional)* **A Python interpreter with the bindings available**, for the + `python` tests only. This is either a build with + `-DFOREFIRE_BUILD_PYTHON=ON`, or an environment where `pip install forefire` + has been run: -```bash - export PYTHONEXE=/path/to/your/python_forefire_enabled -``` + ```bash + export PYTHONEXE=/path/to/your/python + ``` ---- +* The verification scripts under `runff` import `lxml`, `xarray` and + `netCDF4`: -## 2. Running the tests + ```bash + pip install lxml xarray netCDF4 + ``` + +## Running them ```bash - make test +cd tests +bash run.bash # or: make test ``` ---- +`run.bash` skips the `mnh_*` tests when `SRC_MESONH` is unset and the `python` +tests when `PYTHONEXE` is unset, runs `runff` and `runANN` unconditionally, and +prints a pass/fail summary. It exits non-zero if any suite failed. -## 3. Cleaning up +To clean up the outputs (ForeFire dumps, figures, NetCDF files): ```bash -make clean +bash clean.bash # or: make clean ``` -Removes intermediate files (ForeFire outputs, figures, NetCDF files, …) generated by all tests. +## What each set does + +### `mnh_ideal` + +Validates the coupling on a simplified atmospheric profile. Expect a fire front +consistent with the prescribed wind, plus NetCDF and KML outputs. + +### `mnh_real_nested` + +Reproduces a real fire scenario across two nested Meso-NH domains (real forcing +plus a high-resolution nest). It also exercises high-frequency output and the +HTTP web interface, which needs `FOREFIREHOME` set. ---- +### `python` -## 4. Test set details +`run.bash` runs two scripts with `$PYTHONEXE`: -### 4.1 `mnh_ideal` +| Script | What it does | Output | +| --- | --- | --- | +| `percolation.py` | Four fires, each in a band of randomly filled fuel at a different density | `percolation.nc` | +| `idealizedwind.py` | Wind rotating from 0° to 360°, giving a circular front | `360wind.png`, written by ForeFire's own `plot[]` command | -* **Goal** – validate the coupling on a simplified atmospheric profile. -* **Expected outcome** – a fire front consistent with the prescribed wind, plus NetCDF and KML outputs. +The only assertion is that `360wind.png` was produced and is not empty — these +are demonstrations of the API rather than tests of the physics. -### 4.2 `mnh_real_nested` +`farsite_flat.py` is in this directory but is **not** run by `run.bash`. It +compares ForeFire against a FARSITE case, and needs `flatland.lcp`, which is +not in the repository — `python/README.md` has the download URL. -* **Goal** – reproduce a real fire scenario with nested Meso-NH domains. -* **Features** +`test_wheel.py` is not part of this suite either. It is the smoke test +cibuildwheel runs against a built wheel, and is meant to be run against an +installed `forefire`, never from the source tree. - * two domains (real forcing + high-resolution nest); - * performance and stability check. - * high frequency outputs and http web interface (requires FOREFIREHOME environment variable set) - +### `runANN` -### 4.3 `python` +Loads `Rothermel.ffann` — a small network fitted to the Rothermel propagation +model — evaluates it over the fuel, slope and wind combinations in +`modelrun.csv`, and checks the result. -| Script | Description | Expected artefacts | -| ------------------- | -------------------------------------------------------------------- | --------------------------- | -| `idealized_wind.py` | Wind rotating from 0° to 360°; circular fire front (regression test) | `360wind.png`, NetCDF files | -| `percolation.py` | Three fires propagating through randomly heterogeneous fuels | `percolation.nc` | +It needs no machine-learning framework. `ANN_test` is a ForeFire tool built +from `tools/runANN/ANNTest.cpp`, and the `.ffann` format is read by ForeFire's +own evaluator. -### 4.4 `runANN` +> **This suite currently fails.** `run.bash` diffs its output against +> `result.txt.ref`, which is not in the repository, so it exits non-zero on the +> second line every time. See +> [issue #163](https://github.com/forefireAPI/forefire/issues/163). -* **Goal** – verify *ForeFire*’s internal feed-forward routine on an ANN graph trained to approximate the Rothermel formula. -* **Steps** +### `runff` - 1. Load `rothermel.fann`. - 2. Activate on various *fuel / slope / wind* combinations. - 3. Compare with reference outputs. +Exercises the command-line interpreter. There are two entry points, and they do +different things: -### 4.5 `runff` -* **Goal** – verify *ForeFire*’s command line intrerpreter. +* **`run.bash`** — what `tests/run.bash` calls. Three successive scenarios: -Three successive scenarios: + 1. `real_case.ff` — run a real case, write NetCDF output and a `to_reload.ff` + state file; + 2. `reload_case.ff` — reload that state and export KML; + 3. `rungeojson.ff` — load, simulate, export GeoJSON, clear memory, reload the + GeoJSON to verify it. -1. **`realcase.ff`** – start a real case, save the state in FF format. -2. **`reload_case.ff`** Reload the state and export KML. -3. **`rungeojson.ff`** Single-run variant – load ► simulate ► export GeoJSON, clear memory, reload GeoJSON for verification. + It then checks that the expected artefacts exist and are not implausibly + small. ---- +* **`ff-run.bash`** — what CI calls, from `main.yml`, `macos.yml` and + `docker.yml`. It runs the first two scenarios and then compares the KML and + NetCDF against `real_case.kml.ref` and `ForeFire.0.nc.ref` with a numerical + tolerance, using `compare_kml.py` and `compare_nc.py`. This is the one that + can detect physics drift. diff --git a/tests/python/README.md b/tests/python/README.md index 49d433d8..a2748af8 100644 --- a/tests/python/README.md +++ b/tests/python/README.md @@ -1,7 +1,46 @@ -# *Python-basesd* Test Suite +# Python examples and tests -## 1. farsite_flat.py -It runs a simulation based in Farsite software for a north-wind of 3mph. +Everything here needs the `pyforefire` module: either a build configured with +`-DFOREFIRE_BUILD_PYTHON=ON`, or `pip install forefire`. -Required the input .lcp file can be downloaded from: -https://github.com/mbedward/farsite/raw/refs/heads/master/examples/flatland/Inputs/a_lcpFiles/flatland.lcp +`../run.bash` runs only the first two, through `$PYTHONEXE`. + +## `percolation.py` + +Four fires side by side, each in an 80×60 band of fuel filled at random to a +different density (`k_coeffs = [.10, .3, .4, .5]`), so the bands sit either +side of the percolation threshold. Writes `percolation.nc`. A demonstration of +building a heterogeneous fuel map in numpy and handing it to ForeFire with +`addIndexLayer`. + +## `idealizedwind.py` + +One fire under a wind rotating from 0° to 360°, which should trace a circular +front. Writes `360wind.png` through ForeFire's own `plot[]` command — the one +artefact `../run.bash` checks for. + +## `farsite_flat.py` + +Reproduces a FARSITE benchmark case: flat terrain, a 3 mph north wind, seven +hours of spread, compared against the FARSITE result. + +**It cannot run as checked out.** It reads `flatland.lcp`, a landscape file +that is not in this repository. Download it first: + +```bash +curl -LO https://github.com/mbedward/farsite/raw/refs/heads/master/examples/flatland/Inputs/a_lcpFiles/flatland.lcp +``` + +The weather it uses, `flatland_3mph0deg7hr.raws`, *is* in this directory. + +## `test_wheel.py` + +Not part of this suite. It is the smoke test `cibuildwheel` runs against a +built wheel — it checks that the module imports, that its vendored NetCDF +resolves, that the propagation models registered, and that a trivial +simulation advances. Run it against an *installed* `forefire`, never from the +source tree: + +```bash +python tests/python/test_wheel.py +``` From 7002dd8d547427e451a3831d96bf4b9d9259de60 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Thu, 13 Aug 2026 00:15:45 +0200 Subject: [PATCH 26/63] docs(commands): document emit, and de-duplicate clear `emit` has been in Command::makeCmds since it was added, but in neither app/forefire/commands.md nor the command reference. That file is not prose: AdvancedLineEditor.cpp parses it into getCommandMan(), which drives Tab completion, the help text and the syntax colouring. A command missing from it is invisible to the console -- typing `emit` was rendered uncoloured, exactly like a typo, while `save` beside it came out green. commands.md also carried two '## clear' blocks. getCommandMan() assigns cmdMan[key] as it walks the file, so the second silently replaced the first and the longer entry was dead text. They are merged into one that matches what Command::clear does: free the domain, cancel scheduled events, keep the parameters. Both files now cover all 22 registered commands, with no duplicates. Verified: rebuilt and piped `emit`, `clear` and a nonsense word into the console. The first two now colour green as recognised commands, the third does not. Sphinx builds with no new warnings. --- app/forefire/commands.md | 23 +++++++----- docs/source/reference/commands.rst | 58 +++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/app/forefire/commands.md b/app/forefire/commands.md index 1c473ff1..3cf6d191 100644 --- a/app/forefire/commands.md +++ b/app/forefire/commands.md @@ -77,6 +77,17 @@ std::string commandHelp = R"( - 'vel': Velocity vector (vx,vy,vz) associated with the trigger - 't': Time at which the trigger is activated; +## emit + emit[layer=;loc=(x,y,z);radius=;duration=;flux=] + Injects a flux into an existing flux layer, over an area, for a time span starting at the current domain time + Example: emit[layer=heatFlux;loc=(5000,5000,0);radius=50;duration=600;frp=120] + Arguments: + - 'layer': Name of the flux layer to emit into, as given to addLayer. 'name' is accepted as an alias + - 'loc': Centre of the emission in domain coordinates. Alternatives: 'lonlat' in WGS84, or a 'sw'/'ne' box, or a 'swlonlat'/'nelonlat' box + - 'radius': Radius in metres, giving a disc of area pi*r^2. Alternatives: 'surface' or 'area' in m2, or the area of the sw/ne box + - 'duration': Length of the emission in seconds, required. It starts at the current domain time + - 'flux': The emitted quantity per square metre per second, in the layer's own units. Alternatives, any one of: 'frp' in MW, converted using the FRPToWatts parameter and divided by the area; 'value' or 'val', a power divided by the area; 'total', an energy divided by area and duration; + ## goTo goTo[t=seconds] Advances the simulation to the specified time @@ -179,11 +190,9 @@ std::string commandHelp = R"( - 'filename.ff': Filename containing simulation commands to execute ## clear - clear - Clears all simulation data - Example: clear - Arguments: - - Clears the simulation data to reset the state + clear[] + Frees the fire domain and cancels every scheduled event. Parameters are kept, so a new FireDomain can be created without setting them again + Example: clear[] ## systemExec systemExec[command=] @@ -200,10 +209,6 @@ std::string commandHelp = R"( - 'host': Hostname or IP address for the server - 'port': Port number on which the server will listen; -## clear - clear[] - clears-up memory except parameters, no more schedueled events or fire domain. - ## quit quit[] Terminates the simulation diff --git a/docs/source/reference/commands.rst b/docs/source/reference/commands.rst index 7910f1e1..d52eec46 100644 --- a/docs/source/reference/commands.rst +++ b/docs/source/reference/commands.rst @@ -336,6 +336,62 @@ Triggers a change in simulation data at a specific time and location. Can be use trigger[fuelType=wind;vel=(5.0,2.0,0.0);t=1800] # Trigger new wind at t=1800s +.. _cmd-emit: + +``emit`` +~~~~~~~~ + +.. code-block:: none + + emit[layer=;loc=(x,y,z);radius=;duration=;flux=] + +Injects a flux into an existing flux layer, over an area, for a time span +starting at the domain's current time. The layer must already exist — create it +with :ref:`addLayer ` first. + +It lets a source that is not the simulated fire front contribute to a flux +layer. The ``frp`` form takes fire radiative power in megawatts, the unit +satellite products report. + +**Arguments:** + +* ``layer=``: Name of the flux layer to emit into, as given to ``addLayer``. ``name=`` is accepted as an alias. **Required.** +* ``duration=``: Length of the emission, in seconds, starting at the domain's current time. **Required.** + +*Where* — give exactly one of: + +* ``loc=(x,y,z)``: Centre, in domain coordinates. +* ``lonlat=(lon,lat,z)``: Centre, in WGS84. +* ``sw=(x,y,z)`` with ``ne=(x,y,z)``: A box, in domain coordinates. Its centre and area are used. +* ``swlonlat=(lon,lat,z)`` with ``nelonlat=(lon,lat,z)``: The same, in WGS84. + +*Over what area* — from the box if one was given, otherwise: + +* ``radius=``: A disc, of area ``pi * r²``. +* ``surface=`` or ``area=``: The area directly. + +*How much* — give exactly one of: + +* ``flux=``: The emitted quantity per square metre per second, in the layer's own units — W/m² for a heat flux layer. +* ``frp=``: Fire radiative power in megawatts, converted to watts through the ``FRPToWatts`` parameter (1e7 if it is not set) and divided by the area. +* ``value=`` or ``val=``: A total power, divided by the area. +* ``total=``: A total energy, divided by the area and the duration. + +**Example:** + +.. code-block:: none + + addLayer[name=heatFlux;type=flux;modelName=heatFluxBasic] + emit[layer=heatFlux;lonlat=(9.05,41.95,0);radius=50;duration=600;frp=120] + +.. note:: + + Every failure is reported on standard output and the command returns an + error: no active ``FireDomain``, an unknown layer name, a missing + ``duration``, a missing location, or a magnitude given as ``frp``/``value``/ + ``total`` with no usable area. + + .. _cmd-print: ``print`` @@ -486,7 +542,7 @@ Executes ForeFire commands contained within the specified script file. The filen clear[] -Clears all simulation data (domain, fronts, nodes, loaded data) and scheduled events, resetting the simulation state. Parameters are generally kept. +Frees the fire domain — with its fronts, nodes and loaded data — and cancels every scheduled event. Parameters are kept, so a new ``FireDomain`` can be created without setting them again. **Example:** From 75c44412946de2b4c99dfce8ee5a0fbec0b262c5 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Thu, 13 Aug 2026 00:17:54 +0200 Subject: [PATCH 27/63] docs: add what only exists on dev The three cherry-picked commits describe master. This adds what this branch has that master does not: - The unit suite. tests/README.md said five sets of tests; with tests/unit there are six, and it is a CTest suite rather than something run.bash reaches, so the ctest command line is given. - runANN works here (#183). The master text says the suite fails because it diffs against a result.txt.ref that is not in the repository; that is replaced by what it now does -- an RMSE tolerance of 0.05, sitting between the trained network at 0.0235 and a mean-predictor at 0.0966. - test_moisture_invariants.py and test_threading.py exist here. Both are documented, including that test_threading.py does not pass and is not meant to: it is the reproduction for #175 and needs a free-threaded CPython. None of the three are pytest modules, so no pytest command is given. - The four build options this branch added -- FOREFIRE_BUILD_TESTS, FOREFIRE_ENABLE_WARNINGS, FOREFIRE_WARNINGS_AS_ERRORS and FOREFIRE_SANITIZE -- join the table in installation.rst, and the wheel-defaults sentence counts tests among what wheels turn off. --- docs/source/getting_started/installation.rst | 24 +++++++++-- tests/README.md | 38 ++++++++++++----- tests/python/README.md | 45 ++++++++++++++++++-- 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/docs/source/getting_started/installation.rst b/docs/source/getting_started/installation.rst index ebe60466..a91c65db 100644 --- a/docs/source/getting_started/installation.rst +++ b/docs/source/getting_started/installation.rst @@ -227,10 +227,26 @@ time, for example ``cmake -S . -B build -DFOREFIRE_ENABLE_MPI=OFF``. * - ``FOREFIRE_CHECK_LFS`` - ON - Fail early if the Git LFS test fixtures were not pulled. - -Wheel builds flip all six: MPI, native-arch, tools and the LFS check off, -Python and the static core on. That is what makes a wheel run on a machine -other than the one that built it. + * - ``FOREFIRE_BUILD_TESTS`` + - ON + - Build the C++ unit tests and register them with CTest. + * - ``FOREFIRE_ENABLE_WARNINGS`` + - ON + - Compile ForeFire's own sources with ``-Wall -Wextra``. + * - ``FOREFIRE_WARNINGS_AS_ERRORS`` + - OFF + - Fail the build on a compiler warning. + * - ``FOREFIRE_SANITIZE`` + - *(empty)* + - Sanitizers to build with, passed to ``-fsanitize=``, for example + ``address,undefined``. + +Wheel builds flip six of these: MPI, native-arch, tools, tests and the LFS +check off, Python and the static core on. That is what makes a wheel run on a +machine other than the one that built it. + +``TESTING.md`` at the repository root covers the test suites and the sanitizer +build in detail. Making ForeFire available system-wide ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/README.md b/tests/README.md index 34ef8285..ff002083 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,18 +1,27 @@ # ForeFire test suite -Five sets of tests, one per directory, covering the different interfaces and +Six sets of tests, one per directory, covering the different interfaces and use-cases of ForeFire. | Directory | What it covers | Needs | | --- | --- | --- | +| `unit` | C++ tests of one model at a time, without running a simulation, covering every registered propagation and flux model | Nothing beyond the build; see `unit/README.md` | | `mnh_ideal` | ForeFire / **Meso-NH** coupling on an idealised atmospheric case | `SRC_MESONH` set; Meso-NH compiled with the ForeFire library in its `exe` directory | | `mnh_real_nested` | ForeFire / **Meso-NH** coupling on a real nested case | Same as above | | `python` | The Python bindings, through two example simulations | `PYTHONEXE` set to a Python interpreter that can `import pyforefire` | | `runANN` | The built-in feed-forward network evaluator, on a network fitted to Rothermel | `bin/ANN_test`, built by default (`-DFOREFIRE_BUILD_TOOLS=ON`) | | `runff` | The command-line interpreter: run a case, save and reload state, export KML and GeoJSON | ForeFire only | -`TESTING.md` at the repository root describes `runff` — the suite CI actually -gates on — in more detail. +`TESTING.md` at the repository root describes each of these in more detail, +along with the sanitizer build. + +The `unit` suite is not run by `run.bash` below — it is a CTest suite, built +with everything else: + +```bash +cmake -S . -B build && cmake --build build -j +ctest --test-dir build --output-on-failure +``` ## Prerequisites @@ -89,9 +98,15 @@ are demonstrations of the API rather than tests of the physics. compares ForeFire against a FARSITE case, and needs `flatland.lcp`, which is not in the repository — `python/README.md` has the download URL. -`test_wheel.py` is not part of this suite either. It is the smoke test -cibuildwheel runs against a built wheel, and is meant to be run against an -installed `forefire`, never from the source tree. +Three test scripts sit here too, and `run.bash` runs none of them: + +| File | What it checks | Where it runs | +| --- | --- | --- | +| `test_moisture_invariants.py` | That rate of spread responds to dead fuel moisture the way the spread equations require, for every model that reads it | CI, `invariants.yml` | +| `test_threading.py` | That eight concurrent simulations each reproduce the result they give alone | By hand only — it is the failing reproduction for [#175](https://github.com/forefireAPI/forefire/issues/175), and needs a free-threaded CPython | +| `test_wheel.py` | The cibuildwheel smoke test: the module imports, its vendored NetCDF resolves, the models registered, a trivial simulation advances | cibuildwheel, against an *installed* wheel | + +`TESTING.md` gives the command line for each. ### `runANN` @@ -103,10 +118,13 @@ It needs no machine-learning framework. `ANN_test` is a ForeFire tool built from `tools/runANN/ANNTest.cpp`, and the `.ffann` format is read by ForeFire's own evaluator. -> **This suite currently fails.** `run.bash` diffs its output against -> `result.txt.ref`, which is not in the repository, so it exits non-zero on the -> second line every time. See -> [issue #163](https://github.com/forefireAPI/forefire/issues/163). +The check is the root mean squared error between what the network predicts and +what the propagation model produced, against a tolerance of 0.05. That +tolerance sits between the trained network (about 0.024) and a predictor that +ignores its inputs and returns the mean (0.097), so a network that stopped +working would fail. It needs no reference file — the earlier version diffed +against a `result.txt.ref` that was never committed, and so failed on its +second line every time it ran. ### `runff` diff --git a/tests/python/README.md b/tests/python/README.md index a2748af8..9b67b417 100644 --- a/tests/python/README.md +++ b/tests/python/README.md @@ -33,11 +33,48 @@ curl -LO https://github.com/mbedward/farsite/raw/refs/heads/master/examples/flat The weather it uses, `flatland_3mph0deg7hr.raws`, *is* in this directory. -## `test_wheel.py` +## The test scripts -Not part of this suite. It is the smoke test `cibuildwheel` runs against a -built wheel — it checks that the module imports, that its vendored NetCDF -resolves, that the propagation models registered, and that a trivial +These three are tests rather than examples, and `../run.bash` runs none of +them. They are plain scripts, not pytest modules. `TESTING.md` covers each in +detail, including how to build the module they import. + +### `test_moisture_invariants.py` + +Asserts, for every propagation model that consumes dead fuel moisture, that +rate of spread stays finite, falls as moisture rises, reaches zero at the +moisture of extinction, and responds to a dynamic moisture layer. It holds no +reference data — every assertion follows from the published spread equations — +so unlike `runff` it can tell a physics fix from a physics regression. + +Run in CI by `invariants.yml`. Manually: + +```bash +python tests/python/test_moisture_invariants.py # -v for every probe +``` + +`--model NAME` and `--test NAME`, both repeatable, narrow it down. + +### `test_threading.py` + +Runs eight simulations in threads and requires each to reproduce the node +count it produces alone. + +**It does not pass, by design**, and is not run in CI. It is the failing +reproduction for the shared-state problem in +[#175](https://github.com/forefireAPI/forefire/issues/175). It needs a +free-threaded CPython and `PYTHON_GIL=0`; on an ordinary interpreter the GIL +serialises every call and the test skips, reporting a green result that proves +nothing. + +```bash +PYTHON_GIL=0 python3.14t tests/python/test_threading.py +``` + +### `test_wheel.py` + +The smoke test `cibuildwheel` runs against a built wheel: the module imports, +its vendored NetCDF resolves, the propagation models registered, and a trivial simulation advances. Run it against an *installed* `forefire`, never from the source tree: From ff2e68de5779cefa3faa2014157a23c7adda0188 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Thu, 13 Aug 2026 09:15:36 +0200 Subject: [PATCH 28/63] docs(python): document the NumPy array path, no NetCDF or GIS Add a User Guide page walking through a complete simulation driven entirely from NumPy arrays via addIndexLayer/addScalarLayer, with no landscape .nc and no GIS. The worked example was verified against a pip-installed ForeFire. Link it from the index toctree and from the README's Python snippet. Closes #168 --- README.md | 5 + docs/source/index.rst | 1 + docs/source/user_guide/python_arrays.rst | 150 +++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 docs/source/user_guide/python_arrays.rst diff --git a/README.md b/README.md index b70952c1..978845f5 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,11 @@ ff.execute("step[dt=1000]") print(ff.execute("print[]")) ``` +You can drive a whole simulation from NumPy arrays — a fuel map and a wind +field, no NetCDF and no GIS — using `addIndexLayer` and `addScalarLayer`. See +[Running from NumPy arrays](https://forefire.readthedocs.io/en/latest/user_guide/python_arrays.html) +for a complete example. + Published wheels are built without MPI support. For fire-atmosphere coupling with MesoNH, or to tune the build for your CPU, build from source instead (see [Build from source](#build-from-source)). diff --git a/docs/source/index.rst b/docs/source/index.rst index 84ac7a8c..a170d273 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -25,6 +25,7 @@ Welcome to the official documentation for ForeFire — the open-source wildfire user_guide/basic_configuration user_guide/fuels_and_models user_guide/landscape_file + user_guide/python_arrays user_guide/forefire_script user_guide/core_concepts diff --git a/docs/source/user_guide/python_arrays.rst b/docs/source/user_guide/python_arrays.rst new file mode 100644 index 00000000..591981f0 --- /dev/null +++ b/docs/source/user_guide/python_arrays.rst @@ -0,0 +1,150 @@ +.. _userguide-python-arrays: + +Running from NumPy arrays (no NetCDF, no GIS) +============================================= + +ForeFire can run a complete simulation from plain NumPy arrays. You do **not** +need a :ref:`NetCDF landscape file `, and you do not +need any GIS software: build a fuel map and a wind field as arrays in memory, +hand them to the engine, and step the simulation. + +This is the lowest-friction way to use ForeFire, and it is the right entry +point if you install with ``pip install forefire`` and want *NumPy in, NumPy +out* — for example to generate training data for a surrogate model, to sweep +idealized scenarios, or simply to learn the API before wiring up real +geospatial data. + +The two array layers +-------------------- + +Two binding methods take NumPy arrays directly: + +* ``addIndexLayer(name, "fuel", SWx, SWy, SWz, Lx, Ly, Lz, array)`` — the + **fuel index map**. Each cell holds an integer index into the fuel table. +* ``addScalarLayer(name, variable, SWx, SWy, SWz, Lx, Ly, Lz, array)`` — a + continuous field such as a **wind component**. + +The ``SW*`` / ``L*`` arguments give the south-west corner and the extent of the +layer in the simulation's Cartesian metres, so the array is stretched to cover +the domain regardless of its pixel resolution. + +A complete example +------------------ + +The script below ignites a fire at the centre of a 4 km square domain and +drives it with a uniform easterly wind, entirely from arrays. It runs as-is +against a ``pip``-installed ForeFire. + +.. code-block:: python + + import numpy as np + import pyforefire as forefire + from pyforefire import helpers + + ff = forefire.ForeFire() + + # Fuel table, inline — no CSV file needed. The columns depend on the + # propagation model: the WindDriven model used here only needs vv_coeff + # (see "Choosing a fuel table" below). + ff["fuelsTable"] = "Index;vv_coeff;Kcurv;beta\n1;1.0;1.0;1.0\n2;0.3;1.0;1.0" + ff["defaultFuelType"] = 1. + + # Solver parameters. + for key, value in { + "spatialIncrement": 0.5, + "minimalPropagativeFrontDepth": 10, + "perimeterResolution": 10, + "initialFrontDepth": 0.1, + "relax": 1.0, + "smoothing": 0, + "minSpeed": 0.0, + "bmapLayer": 1, + "windReductionFactor": 1.0, + }.items(): + ff[key] = value + ff["propagationModel"] = "WindDriven" + + # Define the domain in Cartesian metres — no NetCDF involved. + Lx = Ly = 4000.0 + ff.execute(f"FireDomain[sw=(0,0,0);ne=({Lx},{Ly},0);t=0]") + + # Build the environment as arrays. The fuel map is a 4-D array shaped + # (1, 1, width, height); the wind components are shaped (1, 2, width, height). + W = H = 200 + fuel_map = np.full((1, 1, W, H), 1.0) # fuel index 1 everywhere + + wind = np.zeros((2, 2, W, H)) + windU = wind[0:1]; windU[0, 0].fill(1.0); windU[0, 1].fill(0.0) + windV = wind[1:2]; windV[0, 0].fill(0.0); windV[0, 1].fill(1.0) + + # Register the propagation model and the array layers. + ff.addLayer("propagation", "WindDriven", "propagationModel") + ff.addIndexLayer("table", "fuel", 0., 0., 0., Lx, Ly, 0., fuel_map) + ff.addScalarLayer("windScalDir", "windU", 0., 0., 0., Lx, Ly, 0., windU) + ff.addScalarLayer("windScalDir", "windV", 0., 0., 0., Lx, Ly, 0., windV) + + # Ignite at the centre and step the simulation, updating the wind each step. + ff.execute("startFire[loc=(2000,2000,0.);t=0]") + + t = 0.0 + for _ in range(8): + ff.execute("trigger[wind;loc=(0.,0.,0.);vel=(8,0,0);t=%f]" % t) + ff.execute("step[dt=30]") + t += 30 + +Getting the result back as an array +----------------------------------- + +The :ref:`print ` command returns the fire front as text, and the +``helpers.printToPathe`` helper turns that into Matplotlib paths whose +``vertices`` are ordinary ``(N, 2)`` NumPy arrays of front coordinates — the +*NumPy out* half of the loop: + +.. code-block:: python + + out = ff.execute("print[]") + pathes = helpers.printToPathe(out) + verts = np.vstack([p.vertices for p in pathes]) + + print("front vertices:", verts.shape) + print("x range: %.0f..%.0f y range: %.0f..%.0f" % ( + verts[:, 0].min(), verts[:, 0].max(), + verts[:, 1].min(), verts[:, 1].max())) + +With the easterly wind above, the front that started as a point at +``(2000, 2000)`` has stretched downwind to roughly ``x = 3900`` after four +minutes of simulated time. You now have the perimeter as an array to plot, +post-process, or feed into whatever comes next. + +Choosing a fuel table +--------------------- + +The columns of ``fuelsTable`` are read by the propagation model, so they change +with the model you select: + +* ``WindDriven`` — a minimal table with ``vv_coeff`` (the rate of spread is + ``vv_coeff × normal wind``), plus ``Kcurv`` and ``beta``. Convenient for + idealized and teaching cases. +* ``Rothermel``, ``BalbiNov2011``, ``Balbi2015`` and the other physical models + — the full fuel description in ``fuels.csv``. See + :ref:`Fuels and Models ` for the columns. + +To make propagation vary across the domain, add rows to the fuel table and +paint their indices into ``fuel_map``. For instance, filling the right half of +the map with a slower fuel: + +.. code-block:: python + + fuel_map[0, 0, :, H // 2:] = 2.0 # index 2 = slower fuel + +Where to go next +--------------- + +* ``tests/python/idealizedwind.py`` in the repository is a fuller worked + version of this example: it drives the same array-based setup with a *turning* + wind and plots each step. +* To run on real terrain instead of arrays, build a + :ref:`NetCDF landscape file ` and load it with + :ref:`loadData `. +* :ref:`Fuels and Models ` explains the fuel table + used by the physical propagation models. From e0d4e1be13c2519bd6086b9b3404bdbba8408b70 Mon Sep 17 00:00:00 2001 From: HugoFara Date: Thu, 13 Aug 2026 09:15:36 +0200 Subject: [PATCH 29/63] feat(python): add a forefire-validate landscape checker Check a landscape .nc against a fuel table before a run: report any fuel index present in the raster but missing from the table, flag a fuel/elevation shape mismatch, and warn when elevation or wind is absent. This is the cheapest fix for the most confusing failure mode, where a mismatch makes the simulation fail or produce wrong results with no message. The decision logic is kept pure and I/O-free (build_report), with a thin netCDF4 adapter (read_landscape) that asks for netCDF4 only when a file must be read, so the wheel gains no hard dependency. Exposed as the forefire-validate console entry point. test_validate.py loads the checker from source and covers the pure logic without the compiled extension; the real-.nc case is skipped when netCDF4 is absent. Wired into run.bash and documented in TESTING.md and the Python README. Closes #169 --- TESTING.md | 10 + bindings/python/README.md | 16 ++ bindings/python/src/pyforefire/_validate.py | 246 ++++++++++++++++++++ pyproject.toml | 4 + tests/python/run.bash | 3 + tests/python/test_validate.py | 220 +++++++++++++++++ 6 files changed, 499 insertions(+) create mode 100644 bindings/python/src/pyforefire/_validate.py create mode 100644 tests/python/test_validate.py diff --git a/TESTING.md b/TESTING.md index b6b02689..0b178eaa 100644 --- a/TESTING.md +++ b/TESTING.md @@ -157,6 +157,16 @@ real spread of rates of spread would make this a much stronger check. `tests/python/` when `PYTHONEXE` is set. It is not itself invoked by CI, which runs the suites individually. +`tests/python/test_validate.py` covers the `forefire-validate` landscape +checker. It loads the checker straight from source and exercises its pure +decision logic, so it needs neither the compiled `_pyforefire` extension nor +`netCDF4`; the one test that reads a real `.nc` is skipped when `netCDF4` is +absent. `run.bash` runs it after the examples. + +```bash +python3 tests/python/test_validate.py +``` + `tests/python/` also holds `idealizedwind.py`, `farsite_flat.py` and `percolation.py`. They are examples rather than tests — they produce plots and assert nothing — and are not run anywhere. They are the closest thing to diff --git a/bindings/python/README.md b/bindings/python/README.md index 6b5a04d2..a55725b3 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -133,6 +133,22 @@ For examples that use real-world data (fuel, topography, wind), see the scripts in the [`tests/python/`](https://github.com/forefireAPI/forefire/tree/master/tests/python) directory of the main repository. +### Validating a landscape file + +A landscape `.nc` and its fuel table have to agree: every fuel index in the +raster must be defined in the table, or the simulation fails or produces wrong +results without saying why. The `forefire-validate` command checks this up +front: + +```bash +forefire-validate landscape.nc fuels.csv +``` + +It reports any fuel index present in the raster but missing from the table, +flags a fuel/elevation shape mismatch, and warns when elevation or wind is +absent. Reading the `.nc` needs the `netCDF4` package (`pip install netCDF4`); +it is not pulled in by the wheel automatically. + --- ## Development diff --git a/bindings/python/src/pyforefire/_validate.py b/bindings/python/src/pyforefire/_validate.py new file mode 100644 index 00000000..7229b06d --- /dev/null +++ b/bindings/python/src/pyforefire/_validate.py @@ -0,0 +1,246 @@ +"""Validate a ForeFire landscape file against a fuel table. + +A landscape ``.nc`` and the fuel table used with it have to agree: every fuel +index painted into the raster must exist in the table, or the simulation "will +likely fail or produce incorrect results". Nothing in the engine reports which +index is missing, so this command does it up front. + +The logic that decides what is wrong is kept pure (:func:`build_report`) and is +tested without any NetCDF file; :func:`read_landscape` is the thin adapter that +pulls the same information out of a real ``.nc`` and needs ``netCDF4``. + +Run it as ``forefire-validate landscape.nc fuels.csv``. +""" + +from __future__ import annotations + +import argparse +import sys +from collections import namedtuple + +# Variable names ForeFire and its documentation accept for each role, in the +# order they are searched. Kept here so the report can name what it looked for. +FUEL_NAMES = ("fuel", "fuel_index", "land_cover") +ELEVATION_NAMES = ("altitude", "elevation", "dem", "hgt") +WIND_U_NAMES = ("windU", "wind_u", "U") +WIND_V_NAMES = ("windV", "wind_v", "V") + +#: A single line of the report. ``level`` is "error", "warning" or "info". +Finding = namedtuple("Finding", ("level", "message")) + +#: What :func:`read_landscape` extracts and :func:`build_report` consumes. +#: ``fuel_name``/``elevation_name`` are the matched variable name or ``None``; +#: ``raster_indices`` is the set of fuel indices found in the raster; +#: ``fuel_shape``/``elevation_shape`` are the spatial shapes or ``None``; +#: ``has_wind_u``/``has_wind_v`` say whether a wind component is present. +Landscape = namedtuple( + "Landscape", + ( + "variables", + "fuel_name", + "raster_indices", + "fuel_shape", + "elevation_name", + "elevation_shape", + "has_wind_u", + "has_wind_v", + ), +) + + +def parse_fuel_indices(text): + """Return the set of ``Index`` values declared in a fuel table. + + Accepts the ``;``-separated ``fuels.csv`` layout (a ``,`` separator is + tolerated as a fallback). The header row is the first non-empty line and is + identified by its first column being ``Index``. + """ + indices = set() + header_seen = False + for raw in text.splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + sep = ";" if ";" in line else "," + first = line.split(sep, 1)[0].strip() + if not header_seen: + header_seen = True + if first.lower() == "index": + continue # skip the header row + # No header: fall through and treat this line as data. + try: + indices.add(int(float(first))) + except ValueError: + # A non-numeric first column that is not the header is not an index. + continue + return indices + + +def _find(names, present): + """Return the first of ``names`` present in ``present``, else ``None``.""" + for name in names: + if name in present: + return name + return None + + +def build_report(landscape, table_indices): + """Compare a :class:`Landscape` against the fuel indices ``table_indices``. + + Returns ``(findings, ok)`` where ``findings`` is a list of :class:`Finding` + and ``ok`` is ``False`` when any finding is an error. This function does no + I/O, so it can be tested with hand-built inputs. + """ + findings = [] + + if landscape.fuel_name is None: + findings.append( + Finding( + "error", + "no fuel index variable found (looked for %s)" + % ", ".join(FUEL_NAMES), + ) + ) + else: + findings.append( + Finding("info", "fuel variable: %r" % landscape.fuel_name) + ) + missing = sorted(landscape.raster_indices - table_indices) + if missing: + findings.append( + Finding( + "error", + "fuel indices in the raster but absent from the table: %s" + % ", ".join(str(i) for i in missing), + ) + ) + else: + findings.append( + Finding( + "info", + "all %d fuel indices are defined in the table" + % len(landscape.raster_indices), + ) + ) + + if landscape.elevation_name is None: + findings.append( + Finding( + "warning", + "no elevation variable (looked for %s); slope will not be " + "computed" % ", ".join(ELEVATION_NAMES), + ) + ) + elif ( + landscape.fuel_shape is not None + and landscape.elevation_shape is not None + and landscape.fuel_shape != landscape.elevation_shape + ): + findings.append( + Finding( + "error", + "fuel %s and elevation %s have different shapes" + % (landscape.fuel_shape, landscape.elevation_shape), + ) + ) + + if not landscape.has_wind_u or not landscape.has_wind_v: + findings.append( + Finding( + "warning", + "no wind field in the file; supply wind via parameters or the " + "trigger command", + ) + ) + + ok = not any(f.level == "error" for f in findings) + return findings, ok + + +def _unique_indices(array): + """Return the set of integer fuel indices in a raster array.""" + import numpy as np + + values = np.ma.compressed(array) if np.ma.isMaskedArray(array) else np.asarray(array) + return {int(v) for v in np.unique(values)} + + +def read_landscape(nc_path): + """Read the fields :func:`build_report` needs from a NetCDF landscape file. + + Requires ``netCDF4``. Raises :class:`SystemExit` with an actionable message + if it is not installed, since it is an optional dependency of the wheel. + """ + try: + import netCDF4 + except ImportError: # pragma: no cover - depends on the environment + raise SystemExit( + "reading a landscape file needs the netCDF4 package: " + "pip install netCDF4" + ) + + with netCDF4.Dataset(nc_path) as ds: + variables = set(ds.variables) + fuel_name = _find(FUEL_NAMES, variables) + elevation_name = _find(ELEVATION_NAMES, variables) + + raster_indices = set() + fuel_shape = None + if fuel_name is not None: + fuel_var = ds.variables[fuel_name] + raster_indices = _unique_indices(fuel_var[:]) + fuel_shape = tuple(int(n) for n in fuel_var.shape if n > 1) + + elevation_shape = None + if elevation_name is not None: + elevation_shape = tuple( + int(n) for n in ds.variables[elevation_name].shape if n > 1 + ) + + return Landscape( + variables=variables, + fuel_name=fuel_name, + raster_indices=raster_indices, + fuel_shape=fuel_shape, + elevation_name=elevation_name, + elevation_shape=elevation_shape, + has_wind_u=_find(WIND_U_NAMES, variables) is not None, + has_wind_v=_find(WIND_V_NAMES, variables) is not None, + ) + + +def format_report(findings): + """Render findings as aligned ``LEVEL: message`` lines.""" + marks = {"error": "ERROR", "warning": "WARN ", "info": "ok "} + return "\n".join("%s %s" % (marks[f.level], f.message) for f in findings) + + +def validate_files(nc_path, fuels_path): + """Validate ``nc_path`` against ``fuels_path``; return ``(findings, ok)``.""" + with open(fuels_path, "r") as handle: + table_indices = parse_fuel_indices(handle.read()) + landscape = read_landscape(nc_path) + return build_report(landscape, table_indices) + + +def main(argv=None): + """Console entry point for ``forefire-validate``.""" + parser = argparse.ArgumentParser( + prog="forefire-validate", + description="Check a ForeFire landscape .nc against a fuel table.", + ) + parser.add_argument("landscape", help="path to the landscape NetCDF file") + parser.add_argument("fuels", help="path to the fuel table (e.g. fuels.csv)") + args = parser.parse_args(argv) + + findings, ok = validate_files(args.landscape, args.fuels) + print(format_report(findings)) + if ok: + print("\nlandscape is consistent with the fuel table") + else: + print("\nlandscape has errors that will break the simulation") + return 0 if ok else 1 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index be4350ec..8ec5bcfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,10 @@ dependencies = [ # See the install rule for `forefire` in CMakeLists.txt for why the binary is # not shipped as a wheel script directly. forefire = "pyforefire._cli:main" +# Check a landscape .nc against a fuel table. Reading the .nc needs netCDF4, +# which is intentionally not a hard dependency; the command asks for it if the +# file has to be read. +forefire-validate = "pyforefire._validate:main" [project.urls] Homepage = "https://forefire.univ-corse.fr/" diff --git a/tests/python/run.bash b/tests/python/run.bash index a4a7ff54..783b851e 100755 --- a/tests/python/run.bash +++ b/tests/python/run.bash @@ -1,6 +1,9 @@ $PYTHONEXE percolation.py $PYTHONEXE idealizedwind.py +# Pure-logic checker for the landscape validator; needs no built extension. +$PYTHONEXE test_validate.py || { echo "test_validate.py failed."; exit 1; } + # Basic sanity checks on generated output # Check that 360wind.png exists and is not empty if [ ! -s 360wind.png ]; then diff --git a/tests/python/test_validate.py b/tests/python/test_validate.py new file mode 100644 index 00000000..45371bf7 --- /dev/null +++ b/tests/python/test_validate.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +"""Tests for the ``forefire-validate`` landscape checker. + +The decision logic in ``pyforefire._validate`` is pure, so it is tested here +with hand-built inputs and no NetCDF file — and the module is loaded straight +from its source path, so this suite does not need the compiled ``_pyforefire`` +extension either. A real ``.nc`` round-trip runs too, but only when ``netCDF4`` +is installed; it is skipped otherwise rather than failing. +""" + +import importlib.util +import os +import sys +import tempfile + +_MODULE_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "bindings", "python", "src", "pyforefire", "_validate.py", +) + + +def _load_validate(): + spec = importlib.util.spec_from_file_location("_ff_validate", _MODULE_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +V = _load_validate() + + +def _landscape(**overrides): + """A consistent Landscape (fuel + elevation + wind, matching shapes).""" + fields = dict( + variables={"fuel", "altitude", "windU", "windV"}, + fuel_name="fuel", + raster_indices={0, 1, 2}, + fuel_shape=(100, 100), + elevation_name="altitude", + elevation_shape=(100, 100), + has_wind_u=True, + has_wind_v=True, + ) + fields.update(overrides) + return V.Landscape(**fields) + + +def _errors(findings): + return [f.message for f in findings if f.level == "error"] + + +def _levels(findings): + return {f.level for f in findings} + + +# --- parse_fuel_indices ----------------------------------------------------- + +def test_parse_semicolon_table(): + text = "Index;Rhod;Ml\n0;563;1.0\n1;563;1.0\n2;614;1.0\n" + got = V.parse_fuel_indices(text) + return [] if got == {0, 1, 2} else [f"parsed {got}, expected {{0,1,2}}"] + + +def test_parse_skips_header_and_blanks(): + text = "\nIndex;a\n1;x\n\n# comment\n7;y\n" + got = V.parse_fuel_indices(text) + return [] if got == {1, 7} else [f"parsed {got}, expected {{1,7}}"] + + +def test_parse_comma_fallback_and_no_header(): + got = V.parse_fuel_indices("3,foo\n4,bar\n") + return [] if got == {3, 4} else [f"parsed {got}, expected {{3,4}}"] + + +# --- build_report ----------------------------------------------------------- + +def test_consistent_case_is_ok(): + findings, ok = V.build_report(_landscape(), {0, 1, 2, 3}) + if not ok: + return [f"expected ok, got errors {_errors(findings)}"] + if "error" in _levels(findings) or "warning" in _levels(findings): + return [f"expected only info, got {_levels(findings)}"] + return [] + + +def test_missing_index_is_error(): + findings, ok = V.build_report(_landscape(raster_indices={0, 1, 9}), {0, 1, 2}) + errs = _errors(findings) + if ok: + return ["index 9 missing from table but case reported ok"] + if not any("9" in m for m in errs): + return [f"error should name index 9, got {errs}"] + return [] + + +def test_absent_fuel_variable_is_error(): + findings, ok = V.build_report( + _landscape(fuel_name=None, raster_indices=set(), fuel_shape=None), + {0, 1}, + ) + if ok or not any("fuel" in m for m in _errors(findings)): + return [f"absent fuel var should be an error, got {_errors(findings)}"] + return [] + + +def test_shape_mismatch_is_error(): + findings, ok = V.build_report( + _landscape(elevation_shape=(50, 50)), {0, 1, 2} + ) + if ok or not any("shape" in m for m in _errors(findings)): + return [f"shape mismatch should be an error, got {_errors(findings)}"] + return [] + + +def test_missing_elevation_is_warning_not_error(): + findings, ok = V.build_report( + _landscape( + variables={"fuel", "windU", "windV"}, + elevation_name=None, + elevation_shape=None, + ), + {0, 1, 2}, + ) + if not ok: + return [f"missing elevation should not be fatal, got {_errors(findings)}"] + if "warning" not in _levels(findings): + return ["missing elevation should produce a warning"] + return [] + + +def test_missing_wind_is_warning_not_error(): + findings, ok = V.build_report( + _landscape( + variables={"fuel", "altitude"}, + has_wind_u=False, + has_wind_v=False, + ), + {0, 1, 2}, + ) + if not ok: + return [f"missing wind should not be fatal, got {_errors(findings)}"] + if "warning" not in _levels(findings): + return ["missing wind should produce a warning"] + return [] + + +# --- read_landscape (only if netCDF4 is available) -------------------------- + +def test_read_real_netcdf(): + try: + import netCDF4 + import numpy as np + except ImportError: + print(" (skipped: netCDF4 not installed)") + return [] + + with tempfile.TemporaryDirectory() as d: + path = os.path.join(d, "case.nc") + with netCDF4.Dataset(path, "w") as ds: + ds.createDimension("y", 4) + ds.createDimension("x", 5) + fuel = ds.createVariable("fuel", "i4", ("y", "x")) + fuel[:] = np.array([[0, 1, 2, 1, 0]] * 4) + alt = ds.createVariable("altitude", "f4", ("y", "x")) + alt[:] = 0.0 + + land = V.read_landscape(path) + problems = [] + if land.fuel_name != "fuel": + problems.append(f"fuel_name={land.fuel_name!r}") + if land.raster_indices != {0, 1, 2}: + problems.append(f"raster_indices={land.raster_indices}") + if land.fuel_shape != (4, 5): + problems.append(f"fuel_shape={land.fuel_shape}") + if land.has_wind_u or land.has_wind_v: + problems.append("wind reported present in a file with none") + + _, ok = V.build_report(land, {0, 1, 2}) + if not ok: + problems.append("consistent real file reported as broken") + return problems + + +def main(): + tests = [ + test_parse_semicolon_table, + test_parse_skips_header_and_blanks, + test_parse_comma_fallback_and_no_header, + test_consistent_case_is_ok, + test_missing_index_is_error, + test_absent_fuel_variable_is_error, + test_shape_mismatch_is_error, + test_missing_elevation_is_warning_not_error, + test_missing_wind_is_warning_not_error, + test_read_real_netcdf, + ] + total = 0 + for fn in tests: + print(f" {fn.__name__}") + try: + failures = fn() + except Exception as exc: + failures = [f"raised {type(exc).__name__}: {exc}"] + if failures: + total += len(failures) + for f in failures: + print(f" FAIL: {f}") + else: + print(" ok") + + print() + if total: + print(f"FAILED: {total} failure(s)") + return 1 + print("All validate tests pass.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From d3c97039ea55de7ba4d46eeb40a1cb8b85d5dd6a Mon Sep 17 00:00:00 2001 From: HugoFara Date: Fri, 14 Aug 2026 15:33:40 +0200 Subject: [PATCH 30/63] build: move stb_image_write.h into third_party/ 1724 lines of public-domain code from nothings/stb sat in src/ among our own, with nothing recording its version, its upstream, or its licence. Patch it and the next update silently reverts the patch. It now sits beside doctest, with all three recorded in the README, and a note that Command.cpp is the only place that may define STB_IMAGE_WRITE_IMPLEMENTATION. third_party/ is now on forefireL's include path as well as the test target's, in both cases as a SYSTEM include, so -Wall -Wextra stops at code we maintain. PRIVATE on forefireL, since Command.cpp is the only consumer. Contributes to #161, which is where the file came to notice: 24 of its 428 warning lines were this one header. --- CMakeLists.txt | 5 +++++ tests/unit/CMakeLists.txt | 4 +++- third_party/README.md | 22 +++++++++++++++++++--- {src => third_party}/stb_image_write.h | 0 4 files changed, 27 insertions(+), 4 deletions(-) rename {src => third_party}/stb_image_write.h (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index a11c7b19..798f6e38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -300,6 +300,11 @@ endif() target_include_directories(forefireL PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/src/include") +# SYSTEM and PRIVATE: vendored headers are ours to use but not ours to fix, so +# the warning flags below must not apply to them, and nothing outside this +# library includes them. +target_include_directories(forefireL SYSTEM PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/third_party") # Threads is explicit because a static core does not drag libpthread in for # its consumers the way the shared library does. find_package(Threads REQUIRED) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 18ecabd9..219e111d 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -16,7 +16,9 @@ add_executable(forefire_unit_tests target_link_libraries(forefire_unit_tests PRIVATE forefireL) target_include_directories(forefire_unit_tests PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}") +# SYSTEM, so the warning flags below stop at code this project maintains. +target_include_directories(forefire_unit_tests SYSTEM PRIVATE "${PROJECT_SOURCE_DIR}/third_party") if(_ff_warning_flags) diff --git a/third_party/README.md b/third_party/README.md index a9bcd837..dd2caeee 100644 --- a/third_party/README.md +++ b/third_party/README.md @@ -3,12 +3,16 @@ Dependencies that are checked in rather than fetched, so that a build and a CI run need no network access beyond cloning the repository. -Nothing here is compiled into `libforefireL`: `CMakeLists.txt` globs `src/*.cpp` -only, and this directory is on the include path of the test target alone. +Nothing here is compiled on its own: `CMakeLists.txt` globs `src/*.cpp` only. +These are headers, included by whichever target needs them. Both `libforefireL` +and the test target have this directory on their include path, in both cases as +a `SYSTEM` include, so the project's `-Wall -Wextra` does not report code we do +not maintain. -| Directory | Version | License | +| File or directory | Version | License | | --- | --- | --- | | `doctest/` | 2.5.3 (2026-07-06) | MIT | +| `stb_image_write.h` | 1.16 | public domain / MIT | ## doctest @@ -22,3 +26,15 @@ it still builds under the project's `CMAKE_CXX_STANDARD 11`. To update it, replace the file with a newer tagged release and run `ctest --test-dir build`. Nothing in `tests/unit/` uses anything beyond `TEST_SUITE`, `TEST_CASE`, `CHECK`, `REQUIRE`, `CAPTURE` and `doctest::Approx`. + +## stb_image_write + +Writes the PNG output of the `plot` command. Taken unmodified from +, v1.16. + +Header-only, and header-only in the awkward sense: the implementation is +compiled in wherever `STB_IMAGE_WRITE_IMPLEMENTATION` is defined before the +include, which `src/Command.cpp` does and nothing else may. + +To update it, replace the file with a newer release and rebuild. ForeFire uses +`stbi_write_png` only. diff --git a/src/stb_image_write.h b/third_party/stb_image_write.h similarity index 100% rename from src/stb_image_write.h rename to third_party/stb_image_write.h From f319083926947307812c3baa8a534321e4549b8b Mon Sep 17 00:00:00 2001 From: HugoFara Date: Fri, 14 Aug 2026 15:34:22 +0200 Subject: [PATCH 31/63] refactor(core): declare copy-assignment on FFPoint and FFVector Both hand-write a copy-constructor and leave assignment to the compiler, so each class carries two independently maintained definitions of how to copy itself. They agree today. They stop agreeing the moment anyone adds a member: the generated assignment picks it up, the hand-written constructor does not, leaving `FFPoint b(a)` and `b = a` producing different objects. FFPoint carries every coordinate in the simulation. Writing the assignment operator out puts both next to each other, so a new member has to be added to both or the omission is visible in one file. Behaviour is unchanged: three doubles each, owning nothing. Contributes to #161, which is where this came to notice: 65 of its 428 warning lines were the missing operator. The issue attributes all 65 to FFPoint; 51 are, the other 14 are FFVector, which has the same shape. --- src/FFPoint.cpp | 10 ++++++++++ src/FFPoint.h | 3 +++ src/FFVector.cpp | 8 ++++++++ src/FFVector.h | 2 ++ 4 files changed, 23 insertions(+) diff --git a/src/FFPoint.cpp b/src/FFPoint.cpp index 0d21c849..a7a8cc73 100644 --- a/src/FFPoint.cpp +++ b/src/FFPoint.cpp @@ -32,6 +32,16 @@ FFPoint::~FFPoint(){ FFPoint::FFPoint(const FFPoint& p) : x(p.x), y(p.y), z(p.z) { // nothing else to do } +// Spelled out rather than left implicit: declaring the copy-constructor above +// deprecates the implicit assignment, so every `a = b` on an FFPoint raises +// -Wdeprecated-copy. The three coordinates own no memory, so copying them is +// exactly what the implicit version did. +FFPoint& FFPoint::operator=(const FFPoint& p){ + x = p.x; + y = p.y; + z = p.z; + return *this; +} // overloading operators const FFPoint operator+(const FFPoint& left, const FFPoint& right){ diff --git a/src/FFPoint.h b/src/FFPoint.h index bb3eb679..10bb8e00 100644 --- a/src/FFPoint.h +++ b/src/FFPoint.h @@ -40,6 +40,9 @@ class FFPoint { /*! \brief Copy-constructor * \param[in] 'p' : point to be copied */ FFPoint(const FFPoint& p); + /*! \brief Copy-assignment + * \param[in] 'p' : point to be copied */ + FFPoint& operator=(const FFPoint& p); /*! \brief overloaded operator + */ friend const FFPoint operator+(const FFPoint&, const FFPoint&); diff --git a/src/FFVector.cpp b/src/FFVector.cpp index cf50fbc2..abe772ec 100644 --- a/src/FFVector.cpp +++ b/src/FFVector.cpp @@ -44,6 +44,14 @@ FFVector::~FFVector() { FFVector::FFVector(const FFVector& v) : vx(v.vx), vy(v.vy), vz(v.vz){ // nothing else to do } +// Same reason as FFPoint::operator=: the copy-constructor above deprecates the +// implicit assignment, and the three components own no memory. +FFVector& FFVector::operator=(const FFVector& v){ + vx = v.vx; + vy = v.vy; + vz = v.vz; + return *this; +} // overloading operators const FFVector operator+(const FFVector& left, const FFVector& right){ diff --git a/src/FFVector.h b/src/FFVector.h index 7e91e296..a1301a2a 100644 --- a/src/FFVector.h +++ b/src/FFVector.h @@ -41,6 +41,8 @@ class FFVector { virtual ~FFVector(); /*! \brief Copy-constructor */ FFVector(const FFVector&); + /*! \brief Copy-assignment */ + FFVector& operator=(const FFVector&); /*! \brief overloaded operator + */ friend const FFVector operator+(const FFVector&, const FFVector&); From 076509024d235e913be88ea8f98513bc39ba5a95 Mon Sep 17 00:00:00 2001 From: Yanis Srairi Date: Mon, 5 Oct 2026 14:14:03 +0200 Subject: [PATCH 32/63] [python] helpers: import matplotlib only in the plotting functions import pyforefire was loading matplotlib even when nothing is plotted. --- CHANGELOG.md | 5 +++++ bindings/python/src/pyforefire/helpers.py | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d77f867..2694f69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,10 @@ Merged since v2.5.0, not yet released. the wheel. ([#154]) - Free-threaded (`cp314t`) wheels are no longer published, because the core is not yet thread-safe; CPython 3.14 is declared supported. ([#155]) +- `import pyforefire` no longer loads matplotlib; it is imported only when + `printToPathe` or `plot_simulation` is called. `plt`, `cm`, `mpath`, + `mpatches` and `ListedColormap` are no longer reachable as `pyforefire` + attributes. ([#193]) ## [v2.5.0] — 2026-08-11 @@ -189,3 +193,4 @@ repository stays easy to return to. [#178]: https://github.com/forefireAPI/forefire/pull/178 [#180]: https://github.com/forefireAPI/forefire/pull/180 [#183]: https://github.com/forefireAPI/forefire/pull/183 +[#193]: https://github.com/forefireAPI/forefire/pull/193 diff --git a/bindings/python/src/pyforefire/helpers.py b/bindings/python/src/pyforefire/helpers.py index 57dedc42..12d1b6f8 100644 --- a/bindings/python/src/pyforefire/helpers.py +++ b/bindings/python/src/pyforefire/helpers.py @@ -1,15 +1,10 @@ import numpy as np -import matplotlib.pyplot as plt -import matplotlib.path as mpath -import matplotlib.patches as mpatches -import matplotlib.cm as cm import math import struct import zlib -from matplotlib.colors import ListedColormap def get_fuels_table(propagation_model): if propagation_model == 'RothermelAndrews2018': @@ -367,6 +362,7 @@ def printToPathe(linePrinted): """ Compute the current results of simulation to pathes. """ + import matplotlib.path as mpath fronts = linePrinted.split("FireFront") pathes = [] for front in fronts[1:]: @@ -539,6 +535,10 @@ def plot_simulation(pathes, fuel_map, elevation_map, myExtents, scalMap = None): Used for plot 4 axis graph, with Heatflux, Fuels, Altitude plotted under simulation, and Statistics for the last axis. """ + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + import matplotlib.cm as cm + from matplotlib.colors import ListedColormap #import seaborn as sns # Create a figure with 2 axis (2 subplots) fig, ax = plt.subplots(figsize=(10,7), dpi=120) From 613c83df60d37ed37b00bcab54bea7c27bd21a54 Mon Sep 17 00:00:00 2001 From: Antonio Leblanc Date: Thu, 15 Oct 2026 23:32:20 -0300 Subject: [PATCH 33/63] [tests] ignore PNG outputs in tests/runff --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 307d470e..fdd1edb1 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,7 @@ tests/python/FF_FS4roslog.csv tests/python/ros_computation.py tests/python/ros_sampling.py tests/python/test_logdb.csv -tests/runff/altitude.png +tests/runff/*.png tests/runff/httpConnectInfo.txt tests/python/log.txt tests/python/runROS.bash From 9e06ade044707ff5eb7e91336a7dfca039aa5543 Mon Sep 17 00:00:00 2001 From: Hugo <72709893+HugoFara@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:13:23 +0200 Subject: [PATCH 34/63] fix(core): stop quit[] from ending the host process (#181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quit is in the script command table, so it is reachable from the Python binding and from the HTTP server, and it called exit(0). A host got no exception and no traceback, its finally blocks and atexit handlers never ran, buffered output was lost, and the status was 0 — so a batch job recorded success having stopped early. quit now releases the session and records the request; quitRequested() exposes it and clearQuitRequest() forgets it. The shell in app/forefire is what leaves: both the interactive loop and the HTTP listen loop check it, so a served quit[] still stops the server. executeLoop stops reading a script after one, rather than the process disappearing mid-file. currentSession.params is no longer deleted. It is the SimulationParameters singleton, owned by GetInstance() and shared with every other holder, so deleting it left GetInstance() handing out a dangling pointer. That went unnoticed only because exit() followed on the next line; without the exit it would be a use-after-free on the next parameter read. The safe topology error path called quit() too, so an internal failure terminated the host on its own, with status 0. It reports and returns error instead. The message moves to stderr and is no longer gated on commandOutputs: a returned error nobody is obliged to check should not also be silent. tests/unit/test_quit_command.cpp covers it, and the CTest entries now require doctest's closing 'Status: SUCCESS!' line. Without that a suite whose process exits mid-run reports as a pass, because ctest sees 0 and does not look for the summary that never printed — the first version of this test passed happily with exit(0) still in place. Verified with the reproduction from #160: before quit[] / AFTER quit[] / FINALLY ran / ATEXIT ran, exit 0 against the previous output, which was 'before quit[]' and a dead interpreter. Unit suite 5/5; runff KML and NetCDF match. Closes #160 Co-authored-by: Antonio Leblanc --- app/forefire/ForeFire.cpp | 14 ++++-- src/Command.cpp | 52 +++++++++++++++++----- src/Command.h | 21 ++++++++- tests/unit/CMakeLists.txt | 12 ++++- tests/unit/test_quit_command.cpp | 75 ++++++++++++++++++++++++++++++++ 5 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 tests/unit/test_quit_command.cpp diff --git a/app/forefire/ForeFire.cpp b/app/forefire/ForeFire.cpp index d9c72bfc..5a36a034 100644 --- a/app/forefire/ForeFire.cpp +++ b/app/forefire/ForeFire.cpp @@ -85,12 +85,14 @@ int ForeFire::startShell(int argc, char* argv[]) { string listenCommand = "listenHTTP[]"; executor.ExecuteCommand(listenCommand); - // Keep the main thread alive indefinitely + // Keep the main thread alive while the server runs. `quit[]` used to + // end the process from inside the library; now it asks, and this is + // the loop that answers, so a served quit[] still stops the server. cout << "(Press Ctrl+C to exit)" << endl; - while (true) { - sleep(3600); + while (!Command::quitRequested()) { + sleep(1); } - + return 0; } else { @@ -160,6 +162,10 @@ void ForeFire::FFShell(ifstream* inputStream) { if (line.empty()) continue; executor.ExecuteCommand(line); + // `quit[]` no longer ends the process from inside the library, so + // the shell is what leaves on request. + if (Command::quitRequested()) + break; } } } diff --git a/src/Command.cpp b/src/Command.cpp index d2509161..49d10920 100644 --- a/src/Command.cpp +++ b/src/Command.cpp @@ -29,6 +29,7 @@ namespace libforefire double Command::endTime = 0; bool Command::firstCommand = true; + bool Command::quitAsked = false; size_t Command::refTabs = 0; FFPoint *Command::lastReadLoc = 0; @@ -1116,13 +1117,14 @@ namespace libforefire } catch (...) { - if (getDomain()->commandOutputs) - { - cout << getDomain()->getDomainID() << ": " - << "**** ERROR IN SAFE TOPOLOGY MODE, QUITING ****" << endl; - } - // TODO supersafe mode ? - quit(arg, numTabs); + // Reports an error rather than ending the process. This + // used to call quit(), so an internal failure here took + // the host down with exit status 0 — a coupled run or a + // batch job recorded success while having stopped early. + // The caller now sees the failure and chooses. + cerr << getDomain()->getDomainID() << ": " + << "**** ERROR IN SAFE TOPOLOGY MODE ****" << endl; + return error; } } } @@ -2423,6 +2425,11 @@ namespace libforefire while (std::getline(*inputStream, line)) { + // A `quit[]` earlier in the script stops the file here rather than + // ending the process, so the caller still gets to return normally. + if (quitAsked) + break; + if (!inTripleQuote) { /* Skip comments and empty lines when *not* inside a multiline literal */ @@ -2959,15 +2966,40 @@ namespace libforefire int Command::quit(const string &arg, size_t &numTabs) { - + // No exit() here. `quit` is in the command table, so it is reachable + // from the Python binding and from the HTTP server, and a library + // that ends its host's process skips every destructor, `finally` and + // atexit handler on the way out — and used to do it with status 0, so + // a batch run reported success having done nothing. The session is + // released and the request is recorded; whoever is driving the shell + // decides what to do about it. delete currentSession.fd; + currentSession.fd = 0; delete currentSession.outStrRep; + currentSession.outStrRep = 0; delete currentSession.sim; - delete currentSession.params; - exit(0); + currentSession.sim = 0; + + // currentSession.params is deliberately not deleted: it is the + // SimulationParameters singleton, owned by GetInstance() and shared + // with every other holder. Deleting it left GetInstance() handing out + // a dangling pointer, which only went unnoticed because exit() + // followed on the next line. + + quitAsked = true; return normal; } + bool Command::quitRequested() + { + return quitAsked; + } + + void Command::clearQuitRequest() + { + quitAsked = false; + } + void Command::setOstringstream(ostringstream *oss) { currentSession.outStream = oss; diff --git a/src/Command.h b/src/Command.h index bd0f8b36..3c9af956 100644 --- a/src/Command.h +++ b/src/Command.h @@ -121,6 +121,8 @@ class Command { }; static bool firstCommand; + /*! \brief set by quit(), read by quitRequested() */ + static bool quitAsked; static size_t refTabs; static FFPoint* lastReadLoc; @@ -176,7 +178,14 @@ class Command { static int systemExec(const string&, size_t&); /*! \brief command to run a system trough pipe */ static int clear(const string&, size_t&); - /*! \brief command to quit the ForeFire shell */ + /*! \brief releases the session and asks the host to stop + * + * Does NOT terminate the process. This is library code, and the host + * process is not ours to end: a `quit[]` arriving over the Python binding + * or the HTTP server used to take the interpreter down with it, skipping + * every destructor and `finally`, and exiting 0 so a batch job reported + * success. Callers driving a shell should check quitRequested() and stop. + */ static int quit(const string&, size_t&); /*! \brief command to quit the ForeFire shell */ static int listenHTTP(const string&, size_t &) ; @@ -232,6 +241,16 @@ class Command { public: + /*! \brief whether a `quit[]` has asked the host to stop + * + * Set by quit() and cleared by clearQuitRequest(). A shell reading + * commands should stop its loop when this is true; a library embedder is + * free to ignore it and carry on, which is the difference between asking + * and terminating. */ + static bool quitRequested(); + /*! \brief forgets a pending quit request */ + static void clearQuitRequest(); + // Reference time static double refTime; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 219e111d..f72c95a8 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -12,7 +12,8 @@ add_executable(forefire_unit_tests test_flux_models.cpp test_http_server.cpp test_model_registry.cpp - test_propagation_models.cpp) + test_propagation_models.cpp + test_quit_command.cpp) target_link_libraries(forefire_unit_tests PRIVATE forefireL) target_include_directories(forefire_unit_tests PRIVATE @@ -27,8 +28,15 @@ endif() # doctest groups its cases into suites; registering one CTest entry per suite # keeps `ctest` output useful without needing doctest's CMake integration. -foreach(_suite "model registry" "propagation models" "flux models" "http server") +foreach(_suite "model registry" "propagation models" "flux models" "http server" + "quit command") string(REPLACE " " "_" _suite_id "${_suite}") add_test(NAME "unit.${_suite_id}" COMMAND forefire_unit_tests --test-suite=${_suite} --no-skipped-summary) + # Requiring doctest's closing line, not just a zero exit status. A test + # that ends the process mid-run — which is exactly what quit[] used to do, + # with exit(0) — otherwise reports as a pass, because ctest sees 0 and the + # summary that never printed is not something it looks for. + set_tests_properties("unit.${_suite_id}" PROPERTIES + PASS_REGULAR_EXPRESSION "\\[doctest\\] Status: SUCCESS!") endforeach() diff --git a/tests/unit/test_quit_command.cpp b/tests/unit/test_quit_command.cpp new file mode 100644 index 00000000..47216a39 --- /dev/null +++ b/tests/unit/test_quit_command.cpp @@ -0,0 +1,75 @@ +/** + * @file test_quit_command.cpp + * @brief `quit[]` must not end the process it is running inside. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + * + * `quit` sits in the script command table, so it is reachable from the Python + * binding and from the HTTP server, and it used to call exit(0). A host got no + * traceback, no exception, no `finally`, no destructors, and a status of 0 — + * a batch job recorded success having stopped early. See #160. + * + * The test for that is the whole file running to completion: if `quit[]` still + * called exit(0), the cases below this one would not report, and ctest would + * see a suite that passed nothing rather than a suite that failed. The + * `--test-suite` entry in CMakeLists is what turns that into a visible result. + */ + +#include "doctest/doctest.h" + +#include "Command.h" + +#include + +using libforefire::Command; + +TEST_SUITE("quit command") { + +TEST_CASE("quit[] returns instead of ending the process") { + Command executor; + Command::clearQuitRequest(); + + std::string command = "quit[]"; + executor.ExecuteCommand(command); + + // Reaching this line at all is the assertion that matters. + CHECK(Command::quitRequested()); +} + +TEST_CASE("the quit request can be cleared and the session reused") { + // A host that decides to ignore the request has to be able to carry on, + // which is the difference between asking and terminating. + Command executor; + Command::clearQuitRequest(); + REQUIRE_FALSE(Command::quitRequested()); + + std::string quitCommand = "quit[]"; + executor.ExecuteCommand(quitCommand); + REQUIRE(Command::quitRequested()); + + Command::clearQuitRequest(); + CHECK_FALSE(Command::quitRequested()); + + // The interpreter still works after a quit: setParameter and getParameter + // round-trip, which they could not do if quit had freed the parameters + // singleton as it used to. + std::string setCommand = "setParameter[ff_test.after.quit=42]"; + executor.ExecuteCommand(setCommand); + CHECK(libforefire::SimulationParameters::GetInstance() + ->getParameter("ff_test.after.quit") == "42"); +} + +TEST_CASE("a second quit is harmless") { + // quit() deletes the session objects and nulls them; running it twice must + // not double free what the first call released. + Command executor; + Command::clearQuitRequest(); + + std::string command = "quit[]"; + executor.ExecuteCommand(command); + executor.ExecuteCommand(command); + + CHECK(Command::quitRequested()); +} + +} /* TEST_SUITE */ From cacd887492abc96a0913fd3b3fa7ef9d05404d9e Mon Sep 17 00:00:00 2001 From: Yanis Srairi <115394890+yanissrairi@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:45:03 +0200 Subject: [PATCH 35/63] fix: clear[] crashed after a fire whose front had stopped (#194) When the last event is taken out of the TimeTable, getUpcomingEvent left head pointing at it. If that event then had an infinite time (its node stopped), insert() deleted it and head dangled, so clear() deleted it a second time. dropAtomEvents could likewise delete the event being processed. Co-authored-by: Antonio Leblanc Co-authored-by: Jean-Baptiste Filippi --- CHANGELOG.md | 3 ++ src/TimeTable.cpp | 1 + tests/unit/CMakeLists.txt | 4 +-- tests/unit/test_timetable.cpp | 52 +++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_timetable.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 2694f69f..40ca86bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,8 @@ Merged since v2.5.0, not yet released. - `StringRepresentation` kept its output buffer, current level and GeoJSON cursor in file-scope globals shared by every instance. They are now members. ([#178]) +- `clear[]` no longer crashes after a simulation whose fire front stopped + everywhere, for instance an ignition in non-burnable fuel. ([#194]) ### Changed @@ -194,3 +196,4 @@ repository stays easy to return to. [#180]: https://github.com/forefireAPI/forefire/pull/180 [#183]: https://github.com/forefireAPI/forefire/pull/183 [#193]: https://github.com/forefireAPI/forefire/pull/193 +[#194]: https://github.com/forefireAPI/forefire/pull/194 diff --git a/src/TimeTable.cpp b/src/TimeTable.cpp index a7a83e96..2c32a614 100644 --- a/src/TimeTable.cpp +++ b/src/TimeTable.cpp @@ -71,6 +71,7 @@ FFEvent* TimeTable::getUpcomingEvent(){ decrement(); } else if ( size() == 1 ) { // this is the only event left + head = nullptr; decrement(); } else { // no events left to be treated (size=0) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index f72c95a8..7a990632 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable(forefire_unit_tests test_http_server.cpp test_model_registry.cpp test_propagation_models.cpp + test_timetable.cpp) test_quit_command.cpp) target_link_libraries(forefire_unit_tests PRIVATE forefireL) @@ -28,8 +29,7 @@ endif() # doctest groups its cases into suites; registering one CTest entry per suite # keeps `ctest` output useful without needing doctest's CMake integration. -foreach(_suite "model registry" "propagation models" "flux models" "http server" - "quit command") +foreach(_suite "model registry" "propagation models" "flux models" "http server" "timetable" "quit command") string(REPLACE " " "_" _suite_id "${_suite}") add_test(NAME "unit.${_suite_id}" COMMAND forefire_unit_tests --test-suite=${_suite} --no-skipped-summary) diff --git a/tests/unit/test_timetable.cpp b/tests/unit/test_timetable.cpp new file mode 100644 index 00000000..f99fd476 --- /dev/null +++ b/tests/unit/test_timetable.cpp @@ -0,0 +1,52 @@ +/** + * @file test_timetable.cpp + * @brief The event queue once its last event has been taken out. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + */ + +#include "doctest/doctest.h" + +#include "FFEvent.h" +#include "TimeTable.h" + +#include + +using libforefire::FFEvent; +using libforefire::TimeTable; + +TEST_SUITE("timetable") { + +TEST_CASE("clear() after the last event was dropped on reinsertion") { + TimeTable table; + table.insert(new FFEvent(nullptr, 1.0, "none")); + + // A node that stopped gets an infinite time, and insert() deletes its event. + FFEvent* last = table.getUpcomingEvent(); + last->setNewTime(std::numeric_limits::infinity()); + table.insert(last); + + table.clear(); + + FFEvent* next = new FFEvent(nullptr, 2.0, "none"); + table.insert(next); + CHECK(table.getUpcomingEvent() == next); + delete next; +} + +TEST_CASE("dropping an atom's events spares the event being processed") { + TimeTable table; + FFEvent* event = new FFEvent(nullptr, 1.0, "none"); + table.insert(event); + + FFEvent* upcoming = table.getUpcomingEvent(); + REQUIRE(upcoming == event); + table.dropAtomEvents(nullptr); + + upcoming->setNewTime(2.0); + table.insert(upcoming); + CHECK(table.getUpcomingEvent() == event); + delete event; +} + +} From 06014dbc7594bd65f9dab082f9b9eff4da0e18f6 Mon Sep 17 00:00:00 2001 From: Hugo <72709893+HugoFara@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:45:21 +0200 Subject: [PATCH 36/63] fix(core): give the propagation models and layers an owner (#182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building and destroying a FireDomain with a propagation layer cost about 183 kB every time, linearly and without limit. Two things were missing an owner, and a third made ownership impossible. The propagative layer was never freed: the delete in ~FireDomain was commented out and the pointer nulled, which also removed any chance of a later cleanup finding it. The models were never freed either. propModelsTable and fluxModelsTable are static, shared by every domain in the process, and nothing cleared them. Each domain now records the entries it registered and releases exactly those, before the data broker goes, since the models hold a pointer to it. The reason that could not simply be added: the FireDomain constructor wiped both tables. Building a second domain therefore dropped the first one's registrations, and its PropagativeLayer was left holding an index that was empty until the new domain registered — at which point it resolved to the *second* domain's model. Command.cpp builds exactly that second domain for coupled runs. The wipe is gone; both tables are static storage and already start out null. Two smaller things on the same path: - getFreePropModelIndex counted down through an unsigned index with no lower bound, so a full table wrapped to SIZE_MAX and read far out of bounds. getFreeFluxModelIndex bound-checked but then returned an occupied index, silently overwriting a live model. Both now report and return an out-of-range value that their callers refuse. - DataBroker::addConstantLayer allocated an array, handed it to a layer that copies it, and dropped it. Both delete[] lines were sitting there commented out, one per branch. Measured over 500 domains, each with a Rothermel layer: RSS growth falls from 91,504 kB to 180 kB, and occupied model slots from perpetually climbing to zero after each domain. Under ASan the unit suite goes from 4.7 MB leaked to none, so the leak check is now blocking for it; runff still leaks ~200 kB on paths the suite does not reach and stays informational. tests/unit/test_domain_ownership.cpp covers the release, the cross-domain clobbering and the loop. The model registry tests no longer delete models by hand, which is a double free now that the domain owns them; each takes its own sandbox instead, so destruction is exercised the way it actually happens. Closes #159 Co-authored-by: Antonio Leblanc Co-authored-by: Jean-Baptiste Filippi --- .github/workflows/sanitizers.yml | 31 ++++---- TESTING.md | 24 ++++-- src/DataBroker.cpp | 7 +- src/FireDomain.cpp | 70 ++++++++++++++--- src/FireDomain.h | 10 +++ tests/unit/CMakeLists.txt | 1 + tests/unit/test_domain_ownership.cpp | 111 +++++++++++++++++++++++++++ tests/unit/test_model_registry.cpp | 35 +++++---- 8 files changed, 239 insertions(+), 50 deletions(-) create mode 100644 tests/unit/test_domain_ownership.cpp diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index aef0c395..72248457 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -9,11 +9,12 @@ name: 🧼 Sanitizers (Linux) # overflow, no double free, on either path. There is no backlog to clear # first, and the point of turning it on now is to keep it that way. See #162. # -# Leaks are a separate matter and are deliberately NOT blocking: nothing owns -# a PropagationModel (#159), so ASan reports every one as leaked. Making that -# fail the build would produce a permanently red job, which teaches everyone -# to ignore it. The leak check runs anyway, as an informational step, so the -# number stays visible and can be made blocking once #159 lands. +# Leaks are blocking for the unit suite. They were not when this job landed — +# nothing owned a PropagationModel, so ASan reported every one as leaked, about +# 4.7 MB in all. #159 gave the models, the propagative layer and two orphaned +# constant-layer arrays an owner, which took the unit suite to zero, so the +# gate went on. runff still leaks ~200 kB on paths the unit suite does not +# reach, so its leak check stays informational until those are found too. # # A separate workflow rather than a job in main.yml because it needs its own # build: install-forefire.sh builds with -O3 -flto -fomit-frame-pointer, which @@ -55,13 +56,16 @@ jobs: cmake -S . -B build-asan -DFOREFIRE_SANITIZE=address cmake --build build-asan -j"$(nproc)" - # Errors are blocking. detect_leaks=0 is the whole reason this can be: - # see the note at the top about #159. + # Leaks are blocking here: the unit suite reports none since the model + # and layer ownership fix in #159, and this is what keeps it that way. - name: Unit tests under ASan env: - ASAN_OPTIONS: detect_leaks=0 + ASAN_OPTIONS: detect_leaks=1 run: ctest --test-dir build-asan --output-on-failure + # runff still leaks about 200 kB over 783 allocations, on paths a single + # simulation reaches and the unit suite does not, so its leak check stays + # informational below. Errors are blocking here as they always were. - name: runff under ASan env: ASAN_OPTIONS: detect_leaks=0 @@ -69,13 +73,14 @@ jobs: cd tests/runff bash ff-run.bash - # Informational only. Prints what is leaking so the figure stays visible - # without gating the build on a known ownership gap. - - name: Leak report (informational) + # Informational only, and only for runff now. Keeps the figure visible so + # the remaining ownership gaps can be found and this can join the gate. + - name: runff leak report (informational) continue-on-error: true env: ASAN_OPTIONS: detect_leaks=1 run: | - echo "::group::Unit suite leaks" - ctest --test-dir build-asan --output-on-failure || true + echo "::group::runff leaks" + cd tests/runff + bash ff-run.bash || true echo "::endgroup::" diff --git a/TESTING.md b/TESTING.md index a6454738..bf1214ae 100644 --- a/TESTING.md +++ b/TESTING.md @@ -210,13 +210,23 @@ ASAN_OPTIONS=detect_leaks=0 ctest --test-dir build-asan --output-on-failure check. Other values are passed straight through — `undefined`, or `address,undefined` for both — but only `address` is currently verified clean. -**`detect_leaks=0` is deliberate, not a workaround.** ForeFire reports zero -ASan *errors* — no use-after-free, no overflow, no double free — on either test -path, which is what makes a blocking job possible. It does leak: nothing owns a -`PropagationModel` (#159), so every one is reported. Leaving leak detection on -would produce a permanently failing job that everyone learns to ignore. The CI -workflow runs the leak check anyway as an informational step, so the number -stays visible, and it can be made blocking once #159 lands. +ForeFire reports zero ASan *errors* — no use-after-free, no overflow, no double +free — on either test path, which is what makes a blocking job possible. + +**Leaks are blocking for the unit suite and informational for `runff`.** The +unit suite reports none: #159 gave the propagation and flux models, the +propagative layer and two orphaned constant-layer arrays an owner, which took +it from about 4.7 MB to zero. Running the suite with leak detection on is +therefore a check rather than a report: + +```bash +ASAN_OPTIONS=detect_leaks=1 ctest --test-dir build-asan --output-on-failure +``` + +`runff` still leaks about 200 kB over 783 allocations, on paths a full +simulation reaches and the unit suite does not, so its leak check runs without +gating the build. Closing those is what would let it join the gate; until then +the figure stays visible in the job log. Note that the sanitizer build writes `bin/forefire` and `lib/libforefireL.so` like any other build, so it replaces a release build in the source tree. diff --git a/src/DataBroker.cpp b/src/DataBroker.cpp index 45e3a790..b67cd5f8 100644 --- a/src/DataBroker.cpp +++ b/src/DataBroker.cpp @@ -1062,7 +1062,9 @@ namespace libforefire XYZTDataLayer *layer = new XYZTDataLayer(name, SWCorner, timeOrigin, spatialExtent, Lt, nx, ny, nz, nt, data); - // delete[] data; + // FFArray copies the values into storage of its own, so this + // array is ours to free and nothing refers to it afterwards. + delete[] data; registerLayer(name, layer); } else if (layertype == "flux") @@ -1081,7 +1083,8 @@ namespace libforefire FluxLayer *layer = new FluxLayer(name, atmoSWCorner, atmoNECorner, atmosphericNx, atmosphericNy, domain->getCells(), data, SWCorner, timeOrigin, spatialExtent, Lt, nx, ny, nz, nt); - // delete[] data; + // Copied by FFArray, same as above. + delete[] data; registerFluxModel(newFluxmodel); registerFluxLayer(name, layer); diff --git a/src/FireDomain.cpp b/src/FireDomain.cpp index 269afcd1..20f17ec5 100644 --- a/src/FireDomain.cpp +++ b/src/FireDomain.cpp @@ -326,13 +326,18 @@ infrontiers.pop_back(); } + // The models this domain registered go back before the broker does: + // they hold a pointer to it, and freeing the broker first would leave + // a destructor reaching through a dangling pointer. + releaseOwnedModels(); + // Deleting data broker and layers if (dataBroker) { delete dataBroker; dataBroker = nullptr; } if (propagativeLayer) { - //delete propagativeLayer; + delete propagativeLayer; propagativeLayer = nullptr; } @@ -695,20 +700,44 @@ params->setParameter(model->getName() + ".keys", properties); //cout<< "loading"<< model->getName()<= NUM_MAX_PROPMODELS ) return false; PropagationModel* model = propModelInstanciation(mindex, mname); if ( model == 0 ) return false; /* Instantiating a flux layer related to this model */ + // Replacing an earlier layer: the old one is this domain's to free, + // and nothing else refers to it. + delete propagativeLayer; propagativeLayer = new PropagativeLayer("ROSlayer", mindex); return true; } @@ -827,6 +856,7 @@ // Otherwise, searching for the model in the available ones size_t mindex = getFreeFluxModelIndex(); + if ( mindex >= NUM_MAX_FLUXMODELS ) return false; string fmname = ""; if ( lname == "heatFlux" ) fmname = "heatFluxBasic"; if ( lname == "vaporFlux" ) fmname = "vaporFluxBasic"; @@ -856,7 +886,16 @@ size_t FireDomain::getFreePropModelIndex(){ size_t mindex = NUM_MAX_PROPMODELS - 1; - while ( propModelsTable[mindex] != 0 ) mindex--; + // Bounded: mindex is unsigned, so walking off the bottom of a full + // table wrapped round to SIZE_MAX and read far out of bounds. + while ( propModelsTable[mindex] != 0 ){ + if ( mindex == 0 ){ + cerr<<"ERROR: no free propagation model slot, max is " + <= NUM_MAX_FLUXMODELS -1){ - cout<<"ERROR No mor flx models allowed, max:"<< NUM_MAX_FLUXMODELS<GetPath(params->getParameter("NetCDFfile")); - - /* initializations for the flux models */ - for ( size_t i = 0; i < NUM_MAX_FLUXMODELS; i++ ) fluxModelsTable[i] = NULL; /* loading the layers for atmospheric variables and coupling variables */ diff --git a/src/FireDomain.h b/src/FireDomain.h index 3debc093..600341bd 100644 --- a/src/FireDomain.h +++ b/src/FireDomain.h @@ -309,6 +309,16 @@ class FireDomain: public ForeFireAtom, Visitable { bool addPropagativeLayer(string); size_t getFreePropModelIndex(); + /*! \brief indices this domain put into the shared model tables + * + * propModelsTable and fluxModelsTable are static, so an entry cannot be + * freed just because some domain is going away — only the domain that + * registered it may. These record which ones are ours. */ + std::vector ownedPropModelIndices; + std::vector ownedFluxModelIndices; + /*! \brief deletes this domain's models and clears their table entries */ + void releaseOwnedModels(); + /* Mesh properties */ /*-----------------*/ FFPoint SWLngLat; /*!< SouthWest Corner of the mesh */ diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 7a990632..b3290f6a 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(forefire_unit_tests main.cpp model_sandbox.cpp + test_domain_ownership.cpp test_flux_models.cpp test_http_server.cpp test_model_registry.cpp diff --git a/tests/unit/test_domain_ownership.cpp b/tests/unit/test_domain_ownership.cpp new file mode 100644 index 00000000..8c68a20d --- /dev/null +++ b/tests/unit/test_domain_ownership.cpp @@ -0,0 +1,111 @@ +/** + * @file test_domain_ownership.cpp + * @brief A FireDomain owns what it registers, and only what it registers. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + * + * propModelsTable and fluxModelsTable are static, shared by every domain in + * the process. Two things used to go wrong with that, both covered here (#159): + * + * - Nothing freed a domain's models or its propagative layer, so every + * domain built and thrown away cost about 183 kB, linearly, forever. + * - The constructor wiped the whole table, so building a second domain + * dropped the first one's registrations. The first domain's layer then + * held an index that was empty, and became the *second* domain's model + * once that one registered. + * + * The second is the one worth keeping a test on: it is silent, it produces + * wrong spread rates rather than a crash, and Command.cpp builds exactly that + * second domain for coupled runs. + */ + +#include "doctest/doctest.h" + +#include "model_sandbox.h" + +#include "FireDomain.h" + +#include + +using libforefire::FireDomain; +using libforefire::FFPoint; +using ff_test::ModelSandbox; + +namespace { + +size_t occupiedPropSlots() { + size_t used = 0; + for (size_t i = 0; i < FireDomain::NUM_MAX_PROPMODELS; i++) + if (FireDomain::propModelsTable[i] != 0) used++; + return used; +} + +FireDomain* makeDomain() { + FFPoint sw(0., 0., 0.); + FFPoint ne(1000., 1000., 0.); + return new FireDomain(0., sw, ne); +} + +} /* anonymous namespace */ + +TEST_SUITE("domain ownership") { + +TEST_CASE("a destroyed domain releases its propagation model") { + ModelSandbox sandbox; // holds the fuel table and restores parameters + const size_t before = occupiedPropSlots(); + + FireDomain* domain = makeDomain(); + REQUIRE(domain->addPropagativeLayer("Rothermel")); + CHECK(occupiedPropSlots() == before + 1); + + delete domain; + // Back to where we started: the slot is free and the model is gone. This + // failing means the table fills up and the memory is never returned. + CHECK(occupiedPropSlots() == before); +} + +TEST_CASE("building a domain leaves another domain's models alone") { + ModelSandbox sandbox; + + FireDomain* first = makeDomain(); + REQUIRE(first->addPropagativeLayer("Rothermel")); + + size_t firstIndex = FireDomain::NUM_MAX_PROPMODELS; + for (size_t i = 0; i < FireDomain::NUM_MAX_PROPMODELS; i++) + if (FireDomain::propModelsTable[i] != 0) firstIndex = i; + REQUIRE(firstIndex < FireDomain::NUM_MAX_PROPMODELS); + const libforefire::PropagationModel* firstModel = + FireDomain::propModelsTable[firstIndex]; + + // The constructor used to clear the table here. + FireDomain* second = makeDomain(); + CHECK(FireDomain::propModelsTable[firstIndex] == firstModel); + + // And the second domain must not be handed the slot the first is using. + REQUIRE(second->addPropagativeLayer("Rothermel")); + CHECK(FireDomain::propModelsTable[firstIndex] == firstModel); + + delete second; + // Destroying the second domain must not take the first one's model with + // it: each releases only what it registered. + CHECK(FireDomain::propModelsTable[firstIndex] == firstModel); + + delete first; +} + +TEST_CASE("many domains do not accumulate table entries") { + // The leak was linear and unbounded, so a loop is the shape of the check. + // Slot occupancy standing still is the observable part of it. + ModelSandbox sandbox; + const size_t before = occupiedPropSlots(); + + for (int i = 0; i < 40; i++) { + FireDomain* domain = makeDomain(); + REQUIRE(domain->addPropagativeLayer("Rothermel")); + delete domain; + } + + CHECK(occupiedPropSlots() == before); +} + +} /* TEST_SUITE */ diff --git a/tests/unit/test_model_registry.cpp b/tests/unit/test_model_registry.cpp index f735c5e8..34607438 100644 --- a/tests/unit/test_model_registry.cpp +++ b/tests/unit/test_model_registry.cpp @@ -122,46 +122,47 @@ TEST_CASE("every model can be destroyed") { // derived destructor as well is a double free, and this case is what says // so: run it under a build that reintroduces one and it aborts. // - // Nothing deletes a model in a normal run — FireDomain keeps them in - // propModelsTable and fluxModelsTable and never frees either — so these - // destructors are reached only from here. That is precisely why the double - // free survived: the code that trips it does not otherwise run. - ModelSandbox sandbox; - + // The models are freed by ~FireDomain, which is what owns them: it records + // the entries it put in the shared model tables and releases those. So + // each model gets its own sandbox here, and destroying the sandbox is the + // destruction under test. Deleting one by hand instead would be the double + // free this case exists to catch. const std::vector& props = propagationModels(); for (size_t i = 0; i < props.size(); i++) { CAPTURE(props[i]); + ModelSandbox sandbox; PropagationModel* model = sandbox.propagation(props[i]); REQUIRE(model != 0); - delete model; } const std::vector& fluxes = fluxModels(); for (size_t i = 0; i < fluxes.size(); i++) { CAPTURE(fluxes[i]); + ModelSandbox sandbox; FluxModel* model = sandbox.flux(fluxes[i]); REQUIRE(model != 0); - delete model; } } -TEST_CASE("destroying a model does not disturb the next one") { +TEST_CASE("destroying a domain's models does not disturb the next domain") { // A double free often shows up as the *next* allocation coming back // corrupted rather than as an immediate abort, so allocate across the // destruction and check the new model is intact. - ModelSandbox sandbox; - - PropagationModel* first = sandbox.propagation("Rothermel"); - REQUIRE(first != 0); - const std::vector wanted = first->wantedProperties; - const size_t count = first->numProperties; - delete first; + std::vector wanted; + size_t count = 0; + { + ModelSandbox sandbox; + PropagationModel* first = sandbox.propagation("Rothermel"); + REQUIRE(first != 0); + wanted = first->wantedProperties; + count = first->numProperties; + } + ModelSandbox sandbox; PropagationModel* second = sandbox.propagation("Rothermel"); REQUIRE(second != 0); CHECK(second->numProperties == count); CHECK(second->wantedProperties == wanted); - delete second; } TEST_CASE("property registration order is stable within a model") { From 6416da0e04c1ddec244fd1aeb8f1cba9ca996439 Mon Sep 17 00:00:00 2001 From: Yanis Srairi <115394890+yanissrairi@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:49:49 +0200 Subject: [PATCH 37/63] fix: clear[] aborted after a fire front merged with itself and was too small (#197) When a front merges with itself, FireFront::merge splits it into two loops. If the loop it keeps has fewer than 5 nodes, it trashed that loop and the front, then returned before the other loop was given a front. Those nodes still pointed at the trashed front and kept moving. When two of them merged, merge ran on the trashed front, found no node and trashed it again, so ~FireDomain deleted it twice. The other loop is now trashed too. In every case seen it had 2 to 4 nodes, below max_inner_front_nodes_filter, so the normal path would have trashed it as well. Co-authored-by: Jean-Baptiste Filippi --- CHANGELOG.md | 1 + src/FireFront.cpp | 8 +++++ tests/unit/CMakeLists.txt | 3 +- tests/unit/model_sandbox.h | 3 ++ tests/unit/test_fire_front.cpp | 63 ++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_fire_front.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ca86bf..b9237c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -196,4 +196,5 @@ repository stays easy to return to. [#180]: https://github.com/forefireAPI/forefire/pull/180 [#183]: https://github.com/forefireAPI/forefire/pull/183 [#193]: https://github.com/forefireAPI/forefire/pull/193 +[#197]: https://github.com/forefireAPI/forefire/pull/197 [#194]: https://github.com/forefireAPI/forefire/pull/194 diff --git a/src/FireFront.cpp b/src/FireFront.cpp index e292708d..08f813c0 100644 --- a/src/FireFront.cpp +++ b/src/FireFront.cpp @@ -757,6 +757,14 @@ void FireFront::merge(FireNode* fna, FireNode* fnb){ if ( outputs ) cout<<"trashing in FireFront::merge : "<toString()<addToTrashNodes(curfn); } + /* The other part, from pa, still belongs to this front: trash it too */ + curfn = pa; + for ( size_t numfn = getNumFN(pa); numfn > 0; numfn-- ){ + next = curfn->getNext(); + if ( outputs ) cout<<"trashing in FireFront::merge : "<toString()<addToTrashNodes(curfn); + curfn = next; + } domain->addToTrashFronts(this); // Scanning the region for burning status diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index b3290f6a..90fc32c8 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(forefire_unit_tests main.cpp model_sandbox.cpp + test_fire_front.cpp test_domain_ownership.cpp test_flux_models.cpp test_http_server.cpp @@ -30,7 +31,7 @@ endif() # doctest groups its cases into suites; registering one CTest entry per suite # keeps `ctest` output useful without needing doctest's CMake integration. -foreach(_suite "model registry" "propagation models" "flux models" "http server" "timetable" "quit command") +foreach(_suite "model registry" "propagation models" "flux models" "http server" "fire front" "timetable" "quit command") string(REPLACE " " "_" _suite_id "${_suite}") add_test(NAME "unit.${_suite_id}" COMMAND forefire_unit_tests --test-suite=${_suite} --no-skipped-summary) diff --git a/tests/unit/model_sandbox.h b/tests/unit/model_sandbox.h index c1a78636..5c104da2 100644 --- a/tests/unit/model_sandbox.h +++ b/tests/unit/model_sandbox.h @@ -52,6 +52,9 @@ class ModelSandbox { * the model reads its coefficients. */ libforefire::SimulationParameters* parameters() const { return params; } + /** The domain the sandbox built. */ + libforefire::FireDomain* fireDomain() const { return domain; } + /** Instantiates a registered propagation model, or returns 0 if the name * is not in the registry. Each call gets a fresh model index. */ libforefire::PropagationModel* propagation(const std::string& name); diff --git a/tests/unit/test_fire_front.cpp b/tests/unit/test_fire_front.cpp new file mode 100644 index 00000000..489eb3c3 --- /dev/null +++ b/tests/unit/test_fire_front.cpp @@ -0,0 +1,63 @@ +/** + * @file test_fire_front.cpp + * @brief A front that merges with itself and is left with too few nodes. + * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. + * @license This program is free software; See LICENSE file for details. (See LICENSE file). + */ + +#include "doctest/doctest.h" + +#include "model_sandbox.h" + +#include "FFPoint.h" +#include "FFVector.h" +#include "FireDomain.h" +#include "FireFront.h" +#include "FireNode.h" +#include "TimeTable.h" + +#include +#include + +using libforefire::FFPoint; +using libforefire::FFVector; +using libforefire::FireDomain; +using libforefire::FireFront; +using libforefire::FireNode; +using libforefire::TimeTable; +using ff_test::ModelSandbox; + +TEST_SUITE("fire front") { + +TEST_CASE("a front trashed when it merges with itself takes all its nodes with it") { + ModelSandbox sandbox; + FireDomain* domain = sandbox.fireDomain(); + TimeTable schedule; + domain->setTimeTable(&schedule); + FireFront* front = domain->addFireFront(0.); + + // Eight nodes on a circle, clockwise, as an expanding front. + std::vector nodes; + FireNode* prev = 0; + for (int i = 0; i < 8; i++) { + double angle = -2. * M_PI * i / 8.; + FFPoint loc(500. + 20. * std::cos(angle), 500. + 20. * std::sin(angle), 0.); + FFVector vel; + prev = domain->addFireNode(loc, vel, 0., 20., 0., front, prev); + nodes.push_back(prev); + } + REQUIRE(front->getNumFN() == 8); + REQUIRE(front->getArea() > 0.); + + // Merging nodes 0 and 4 splits the front into the loop it keeps, nodes 1 + // to 4, and an inner loop, nodes 5 to 7. The kept loop has fewer than 5 + // nodes, so the front is trashed. + front->merge(nodes[0], nodes[4]); + + for (size_t i = 0; i < nodes.size(); i++) { + CAPTURE(i); + CHECK(nodes[i]->getFront() != front); + } +} + +} From d340c12a6d880d72ffa595680f7661c532c916bc Mon Sep 17 00:00:00 2001 From: Yanis Srairi <115394890+yanissrairi@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:52:09 +0200 Subject: [PATCH 38/63] fix: segfault when a fire front was reduced to two nodes (#198) When a front is down to two nodes, FireDomain::checkTopology trashes both of them, but clears their front first, so eraseTopology could not move the front's head off them. The front then kept a trashed node as its head: the next burning scan counted its nodes from there, getNumFN failed, trashed the front and returned 1, and storeVertices read the null head. The head is now reset, as FireFront::dropFireNode does for a front of two nodes. Co-authored-by: Jean-Baptiste Filippi --- CHANGELOG.md | 3 +++ src/FireDomain.cpp | 4 +++ tests/unit/CMakeLists.txt | 3 ++- tests/unit/test_topology.cpp | 49 ++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_topology.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index b9237c82..6811c08f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,8 @@ Merged since v2.5.0, not yet released. - `StringRepresentation` kept its output buffer, current level and GeoJSON cursor in file-scope globals shared by every instance. They are now members. ([#178]) +- A fire front reduced to two nodes kept a deleted node as its head, and the + next burning scan crashed the simulation. ([#198]) - `clear[]` no longer crashes after a simulation whose fire front stopped everywhere, for instance an ignition in non-burnable fuel. ([#194]) @@ -196,5 +198,6 @@ repository stays easy to return to. [#180]: https://github.com/forefireAPI/forefire/pull/180 [#183]: https://github.com/forefireAPI/forefire/pull/183 [#193]: https://github.com/forefireAPI/forefire/pull/193 +[#198]: https://github.com/forefireAPI/forefire/pull/198 [#197]: https://github.com/forefireAPI/forefire/pull/197 [#194]: https://github.com/forefireAPI/forefire/pull/194 diff --git a/src/FireDomain.cpp b/src/FireDomain.cpp index 20f17ec5..a10fde54 100644 --- a/src/FireDomain.cpp +++ b/src/FireDomain.cpp @@ -2139,10 +2139,14 @@ return; } else { if ( fn->getPrev()->getNext() == fn ){ + FireFront* ff = fn->getFront(); + FireNode* prev = fn->getPrev(); fn ->setFront(0); fn->getPrev()->setFront(0); addToTrashNodes(fn->getPrev()); addToTrashNodes(fn); + // these two nodes were the whole front + if ( ff and ( ff->getHead() == fn or ff->getHead() == prev ) ) ff->setHead(0); return; } else { debugOutput<setTimeTable(&schedule); + FireFront* front = domain->addFireFront(0.); + + FFVector vel; + FFPoint a(500., 500., 0.), b(510., 500., 0.); + FireNode* first = domain->addFireNode(a, vel, 0., 20., 0., front); + domain->addFireNode(b, vel, 0., 20., 0., front, first); + REQUIRE(front->getNumFN() == 2); + REQUIRE(front->getHead() == first); + + // Previous and next are the same node: both nodes are trashed. + domain->checkTopology(first); + + CHECK(front->getHead() == nullptr); +} + +} From 61d9428692343f51596ade3fdbdfc0ad9af25f16 Mon Sep 17 00:00:00 2001 From: Antonio Leblanc <56365887+antonio-leblanc@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:42:16 -0300 Subject: [PATCH 39/63] build: remove two stray parentheses from the unit test list #194 and #198 were merged on top of #181, each keeping its own closing parenthesis, so CMake failed to configure tests/unit. --- tests/unit/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 81152689..4b99bc37 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -15,8 +15,8 @@ add_executable(forefire_unit_tests test_http_server.cpp test_model_registry.cpp test_propagation_models.cpp - test_topology.cpp) - test_timetable.cpp) + test_topology.cpp + test_timetable.cpp test_quit_command.cpp) target_link_libraries(forefire_unit_tests PRIVATE forefireL) From 3caee53ca96fb70e64920bb1c60cecfbfea7a88f Mon Sep 17 00:00:00 2001 From: Antonio Leblanc <56365887+antonio-leblanc@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:01:15 -0300 Subject: [PATCH 40/63] fix(propagation): divide by the surface-to-volume ratio in Balbi2020 eq. C7 (#201) /2*ls multiplied by ls instead of dividing, so Chi was close to zero. With the radiant fraction right, a downslope with no wind gives a negative rate of spread, so it is floored at zero. Fixes #196 --- CHANGELOG.md | 5 +++++ src/propagation/Balbi2020.cpp | 8 +++++--- tests/unit/test_propagation_models.cpp | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6811c08f..7fe0f3e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,10 @@ Merged since v2.5.0, not yet released. next burning scan crashed the simulation. ([#198]) - `clear[]` no longer crashes after a simulation whose fire front stopped everywhere, for instance an ignition in non-burnable fuel. ([#194]) +- `Balbi2020` multiplied by the fuel surface-to-volume ratio instead of + dividing by it in eq. C7, which drove the radiant fraction to zero. It now + matches `Balbi2020.py` in `wildfire_ROS_models`, and its rate of spread is + floored at zero on a downslope with no wind. ([#196]) ### Changed @@ -201,3 +205,4 @@ repository stays easy to return to. [#198]: https://github.com/forefireAPI/forefire/pull/198 [#197]: https://github.com/forefireAPI/forefire/pull/197 [#194]: https://github.com/forefireAPI/forefire/pull/194 +[#196]: https://github.com/forefireAPI/forefire/issues/196 diff --git a/src/propagation/Balbi2020.cpp b/src/propagation/Balbi2020.cpp index b9f66654..ba8f429e 100644 --- a/src/propagation/Balbi2020.cpp +++ b/src/propagation/Balbi2020.cpp @@ -223,7 +223,7 @@ double Balbi2020::getSpeed(double* valueOf){ double p = (2/lr00)/ltau0 ; // derived from expression between C7 and C8 // Radiant fractor - double Chi = lChi0 /(1+p*((R*ltau0*cos(lalpha))/2*ls)) ; // eq. C7 + double Chi = lChi0 /(1+p*((R*ltau0*cos(lalpha))/(2*ls))) ; // eq. C7 //print("Radiant Fraction ", Chi) // Mean Flame Temperature @@ -265,12 +265,14 @@ double Balbi2020::getSpeed(double* valueOf){ } stopcondition = (abs(error) > maxEps); } + // eq. 27 makes Rc negative on a downslope with no wind, and Rb no longer + // covers it once Chi is right: a fire spreads slowly there, not backwards. if (flag==1){ - return Rnew; + return max(0., Rnew); } //cout << "R "<< R << " not convergent " << error << endl; - return R+error/2; + return max(0., R+error/2); } } /* namespace libforefire */ diff --git a/tests/unit/test_propagation_models.cpp b/tests/unit/test_propagation_models.cpp index cac7d21c..ce1f9725 100644 --- a/tests/unit/test_propagation_models.cpp +++ b/tests/unit/test_propagation_models.cpp @@ -251,7 +251,7 @@ TEST_CASE("recorded rates of spread for the standard fuel") { const Expectation expected[] = { {"Rothermel", 0.369131}, {"Balbi2015", 0.069659}, - {"Balbi2020", 0.0783659}, + {"Balbi2020", 0.0823161}, {"BalbiNov2011", 8.16139e-05}, {"BalbiNov2011Curv", 0.080263}, {"BalbiNov2011TMdMl", 0.0584482}, From b1dd86d686219da9af714aae432e4aa4f6f4a8a7 Mon Sep 17 00:00:00 2001 From: Hugo <72709893+HugoFara@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:05:16 +0200 Subject: [PATCH 41/63] feat(python): run a simulation in its own worker process (#200) ForeFire keeps simulation state in process-wide statics, so two ForeFire() objects in one Python process share it. ForeFireRunner gives each simulation a separate process instead, as discussed in #175, and leaves the kernel and ForeFire() unchanged. The worker is a fresh interpreter (python -m pyforefire._worker) started with fork+exec, since the caller may have threads. Calls are pickled over a socket pair, numpy arrays included. Exceptions in the worker are raised in the caller, and a dead worker raises WorkerDied instead of ending Python. There is no fork of a running simulation yet. --- .github/workflows/invariants.yml | 3 + CHANGELOG.md | 4 + TESTING.md | 13 ++ bindings/python/src/pyforefire/__init__.py | 1 + bindings/python/src/pyforefire/_runner.py | 110 +++++++++++++++ bindings/python/src/pyforefire/_worker.py | 34 +++++ tests/python/README.md | 13 +- tests/python/test_runner.py | 154 +++++++++++++++++++++ 8 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 bindings/python/src/pyforefire/_runner.py create mode 100644 bindings/python/src/pyforefire/_worker.py create mode 100644 tests/python/test_runner.py diff --git a/.github/workflows/invariants.yml b/.github/workflows/invariants.yml index abbd5f98..a62dcb53 100644 --- a/.github/workflows/invariants.yml +++ b/.github/workflows/invariants.yml @@ -52,3 +52,6 @@ jobs: - name: Run dead fuel moisture invariants run: ./.venv/bin/python tests/python/test_moisture_invariants.py + + - name: Run the worker process runner test + run: ./.venv/bin/python tests/python/test_runner.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe0f3e6..573aef03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,10 @@ Merged since v2.5.0, not yet released. tests. ([#156]) - `TESTING.md` now documents every suite, how to run it, and which ones CI validates. +- `pyforefire.ForeFireRunner`, a `ForeFire` that runs in its own worker + process, so several simulations in one Python process no longer share + state, and a crash in the kernel raises `pyforefire.WorkerDied` instead of + ending Python. `ForeFire()` itself is unchanged. ([#175]) ### Fixed diff --git a/TESTING.md b/TESTING.md index bf1214ae..adffffdc 100644 --- a/TESTING.md +++ b/TESTING.md @@ -111,6 +111,19 @@ which is the point — it is the failing test the work in #175 has to make pass. Wiring it into CI belongs with the last step of that issue, once it can pass for the right reason. +## Running the Worker Process Test (`test_runner.py`) + +Checks `pyforefire.ForeFireRunner`, which runs each simulation in its own +worker process (#175). Eight runners driven from threads must each reproduce +the in-process fire node count; exceptions in the worker must reach the +caller, and a killed worker must raise `WorkerDied`. It needs no free-threaded +interpreter, because the runners are separate processes. CI runs it in the +Model Invariants workflow. + +```bash +./.venv/bin/python tests/python/test_runner.py +``` + ## Running the ANN Test (`runANN`) Validated in CI by `main.yml`. It is the only coverage for diff --git a/bindings/python/src/pyforefire/__init__.py b/bindings/python/src/pyforefire/__init__.py index 121d4f6d..0fd7dea3 100644 --- a/bindings/python/src/pyforefire/__init__.py +++ b/bindings/python/src/pyforefire/__init__.py @@ -2,5 +2,6 @@ from .helpers import * from ._pyforefire import * # Import the C++ extension +from ._runner import ForeFireRunner, WorkerDied __all__ = ['helpers', '_pyforefire'] \ No newline at end of file diff --git a/bindings/python/src/pyforefire/_runner.py b/bindings/python/src/pyforefire/_runner.py new file mode 100644 index 00000000..5275088c --- /dev/null +++ b/bindings/python/src/pyforefire/_runner.py @@ -0,0 +1,110 @@ +"""Run a ForeFire simulation in its own worker process. + +ForeFire keeps simulation state in process-wide statics, so two `ForeFire()` +objects in one process share it. `ForeFireRunner` gives each simulation a +separate process instead, which isolates its memory and leaves the kernel +unchanged (see issue #175). + +The worker is a fresh interpreter (`python -m pyforefire._worker `) rather +than a fork of the caller, because the caller may have threads. Requests and +replies are pickled over a socket pair. The worker's stdout and stderr are the +caller's, so kernel output still shows up where it did before. + + runner = ForeFireRunner() + runner.execute("FireDomain[sw=(0,0,0);ne=(1000,1000,0);t=0]") + runner["propagationModel"] = "Iso" + runner.close() +""" + +import socket +import subprocess +import sys +import threading +from multiprocessing.connection import Connection + +__all__ = ["ForeFireRunner", "WorkerDied"] + + +class WorkerDied(RuntimeError): + """The worker process ended while a call was pending, or before it.""" + + +class ForeFireRunner: + """A `pyforefire.ForeFire` that lives in a separate process. + + Every method of `ForeFire` is forwarded, along with `runner[key]` and + `runner[key] = value`. Arguments and results are copied between the two + processes, numpy arrays included. An exception raised in the worker is + raised again here; a crash of the worker raises `WorkerDied` instead of + ending the caller. + + Calls on one runner are serialised by a lock. Separate runners do not + share anything and can be driven from separate threads. + """ + + def __init__(self): + parent, child = socket.socketpair() + try: + self._process = subprocess.Popen( + [sys.executable, "-m", "pyforefire._worker", str(child.fileno())], + pass_fds=(child.fileno(),), + ) + finally: + child.close() + self._conn = Connection(parent.detach()) + self._lock = threading.Lock() + + def _call(self, name, *args): + with self._lock: + if self._conn is None: + raise WorkerDied("the runner is closed") + try: + self._conn.send((name, args)) + ok, value = self._conn.recv() + except (EOFError, OSError): + code = self._process.wait() + self._conn.close() + self._conn = None + raise WorkerDied(f"the ForeFire worker exited with status {code}") from None + if not ok: + raise value + return value + + def __getattr__(self, name): + if name.startswith("_"): + raise AttributeError(name) + return lambda *args: self._call(name, *args) + + def __getitem__(self, key): + return self._call("__getitem__", key) + + def __setitem__(self, key, value): + self._call("__setitem__", key, value) + + def close(self): + """Stop the worker and wait for it. Calling it twice is harmless.""" + with self._lock: + if self._conn is not None: + try: + self._conn.send(None) + except OSError: + pass + self._conn.close() + self._conn = None + if self._process.poll() is None: + try: + self._process.wait(timeout=10) + except subprocess.TimeoutExpired: + self._process.kill() + self._process.wait() + + def __enter__(self): + return self + + def __exit__(self, *exc): + self.close() + + def __del__(self): + if getattr(self, "_process", None) is not None: + self.close() + diff --git a/bindings/python/src/pyforefire/_worker.py b/bindings/python/src/pyforefire/_worker.py new file mode 100644 index 00000000..eec95777 --- /dev/null +++ b/bindings/python/src/pyforefire/_worker.py @@ -0,0 +1,34 @@ +"""Worker side of `ForeFireRunner`: `python -m pyforefire._worker `. + +Owns one `ForeFire` and answers the requests pickled over socket `fd` until +the caller sends None or goes away. +""" + +import sys +from multiprocessing.connection import Connection + + +def _serve(conn): + from ._pyforefire import ForeFire + + ff = ForeFire() + while True: + try: + request = conn.recv() + except EOFError: + return + if request is None: + return + name, args = request + try: + reply = (True, getattr(ff, name)(*args)) + except Exception as e: + reply = (False, e) + try: + conn.send(reply) + except Exception as e: # an unpicklable result or exception + conn.send((False, RuntimeError(f"{name}: {e}"))) + + +if __name__ == "__main__": + _serve(Connection(int(sys.argv[1]))) diff --git a/tests/python/README.md b/tests/python/README.md index 9b67b417..fb952c43 100644 --- a/tests/python/README.md +++ b/tests/python/README.md @@ -35,7 +35,7 @@ The weather it uses, `flatland_3mph0deg7hr.raws`, *is* in this directory. ## The test scripts -These three are tests rather than examples, and `../run.bash` runs none of +These four are tests rather than examples, and `../run.bash` runs none of them. They are plain scripts, not pytest modules. `TESTING.md` covers each in detail, including how to build the module they import. @@ -55,6 +55,17 @@ python tests/python/test_moisture_invariants.py # -v for every probe `--model NAME` and `--test NAME`, both repeatable, narrow it down. +### `test_runner.py` + +Checks `pyforefire.ForeFireRunner`: eight simulations, each in its own worker +process and driven from a thread, must match the in-process node count, and +worker errors and crashes must surface as Python exceptions. Run in CI by +`invariants.yml`. Manually: + +```bash +python tests/python/test_runner.py +``` + ### `test_threading.py` Runs eight simulations in threads and requires each to reproduce the node diff --git a/tests/python/test_runner.py b/tests/python/test_runner.py new file mode 100644 index 00000000..ba641bd1 --- /dev/null +++ b/tests/python/test_runner.py @@ -0,0 +1,154 @@ +"""Tests for `pyforefire.ForeFireRunner`, one simulation per worker process. + +Unlike `test_threading.py`, this needs no free-threaded interpreter: each +runner is its own process, so threads driving separate runners overlap even +with the GIL on. + + python tests/python/test_runner.py + +Exit status is 0 only if every check passes. +""" + +import os +import signal +import sys +import threading +import traceback + +SIZE = 1000 +STEPS = 5 +RUNNERS = 8 + + +def run_simulation(ff): + """One self-contained simulation. Returns its fire node count.""" + ff.execute(f"FireDomain[sw=(0,0,0);ne=({SIZE},{SIZE},0);t=0]") + ff.addLayer("propagation", "Iso", "propagationModel") + ff.execute(f"startFire[loc=({SIZE / 2},{SIZE / 2},0)]") + for _ in range(STEPS): + ff.execute("step[dt=100]") + return ff.execute("print[]").count("FireNode") + + +def test_matches_in_process(baseline): + """A runner must give exactly what the in-process object gives.""" + import pyforefire + + with pyforefire.ForeFireRunner() as runner: + got = run_simulation(runner) + if got != baseline: + return [f"runner produced {got} fire nodes, expected {baseline}"] + return [] + + +def test_concurrent_runners(baseline): + """Runners driven from separate threads must not disturb each other.""" + import pyforefire + + results = [None] * RUNNERS + errors = [None] * RUNNERS + + def worker(i): + try: + with pyforefire.ForeFireRunner() as runner: + results[i] = run_simulation(runner) + except BaseException: + errors[i] = traceback.format_exc() + + threads = [threading.Thread(target=worker, args=(i,)) for i in range(RUNNERS)] + for t in threads: + t.start() + for t in threads: + t.join() + + failures = [] + for i, err in enumerate(errors): + if err is not None: + failures.append(f"runner {i} raised:\n{err}") + for i, got in enumerate(results): + if errors[i] is None and got != baseline: + failures.append(f"runner {i} produced {got} fire nodes, expected {baseline}") + return failures + + +def test_parameters(): + """`runner[key]` reads and writes the worker's parameters.""" + import pyforefire + + failures = [] + with pyforefire.ForeFireRunner() as runner: + runner["perimeterResolution"] = 7.5 + if runner["perimeterResolution"] != 7.5: + failures.append(f"read back {runner['perimeterResolution']!r}, expected 7.5") + runner.setString("outputsFormat", "json") + if runner.getString("outputsFormat") != "json": + failures.append("setString/getString did not round-trip") + return failures + + +def test_errors_are_raised_here(): + """Exceptions in the worker reach the caller, and the worker survives them.""" + import pyforefire + + failures = [] + with pyforefire.ForeFireRunner() as runner: + # Without a domain, reading a missing key segfaults instead of raising. + runner.execute(f"FireDomain[sw=(0,0,0);ne=({SIZE},{SIZE},0);t=0]") + try: + runner.noSuchMethod() + failures.append("calling a missing method did not raise") + except AttributeError: + pass + try: + runner["noSuchParameter"] + failures.append("reading a missing parameter did not raise") + except RuntimeError: + pass + runner["perimeterResolution"] = 3.0 + if runner["perimeterResolution"] != 3.0: + failures.append("the worker stopped answering after an error") + return failures + + +def test_worker_death(): + """A dead worker raises WorkerDied instead of taking the caller down.""" + import pyforefire + + runner = pyforefire.ForeFireRunner() + os.kill(runner._process.pid, signal.SIGKILL) + runner._process.wait() + failures = [] + for attempt in ("first", "second"): + try: + runner.execute("print[]") + failures.append(f"the {attempt} call on a dead worker did not raise") + except pyforefire.WorkerDied: + pass + runner.close() + return failures + + +def main(): + import pyforefire + + baseline = run_simulation(pyforefire.ForeFire()) + print(f"in-process baseline: {baseline} fire nodes") + + failures = [] + failures += test_matches_in_process(baseline) + failures += test_concurrent_runners(baseline) + failures += test_parameters() + failures += test_errors_are_raised_here() + failures += test_worker_death() + + if failures: + print(f"\nFAILED with {len(failures)} problem(s):") + for f in failures: + print(f" - {f}") + return 1 + print(f"OK: {RUNNERS} concurrent runners matched the baseline; errors and crashes surfaced") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 648e5b41433b6ef239b0b845962ff78df945f25c Mon Sep 17 00:00:00 2001 From: Hugo <72709893+HugoFara@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:43:12 +0200 Subject: [PATCH 42/63] fix(python): raise instead of segfaulting before FireDomain[...] (#202) * fix(python): raise instead of segfaulting before FireDomain[...] Seven calls in the binding dereferenced the domain without checking it, so ff["key"], addLayer, addScalarLayer, addIndexLayer or getDoubleArray before FireDomain[...] crashed the Python process. They now go through requireDomain(), which raises RuntimeError. test_wheel.py checks each of them before creating a domain. * docs(changelog): link #202 --- CHANGELOG.md | 4 +++ .../python/src/pyforefire/_pyforefire.cpp | 26 +++++++++++----- tests/python/test_wheel.py | 31 +++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 573aef03..b178551c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,9 @@ Merged since v2.5.0, not yet released. ### Fixed +- `pyforefire` calls that read the domain (`ff[key]`, `addLayer`, + `addScalarLayer`, `addIndexLayer`, `getDoubleArray`) segfaulted Python when + run before `FireDomain[...]`. They now raise `RuntimeError`. ([#202]) - Rate of spread stayed finite and decreasing at high dead fuel moisture, and `DataBroker` no longer serves `moisture` through the five-slot getter, which returned a neighbouring property. ([#158]) @@ -210,3 +213,4 @@ repository stays easy to return to. [#197]: https://github.com/forefireAPI/forefire/pull/197 [#194]: https://github.com/forefireAPI/forefire/pull/194 [#196]: https://github.com/forefireAPI/forefire/issues/196 +[#202]: https://github.com/forefireAPI/forefire/pull/202 diff --git a/bindings/python/src/pyforefire/_pyforefire.cpp b/bindings/python/src/pyforefire/_pyforefire.cpp index 8aa84e06..b1d6d8ef 100644 --- a/bindings/python/src/pyforefire/_pyforefire.cpp +++ b/bindings/python/src/pyforefire/_pyforefire.cpp @@ -10,6 +10,16 @@ Command* pyxecutor; Command::Session* session; SimulationParameters* params; +// Every call below that reads the domain would dereference a null pointer +// before FireDomain[...] has run, so raise a Python exception instead. +static FireDomain* requireDomain() { + FireDomain* fd = pyxecutor->getDomain(); + if (fd == nullptr) { + throw std::runtime_error("no FireDomain yet: execute FireDomain[...] first"); + } + return fd; +} + PLibForeFire::PLibForeFire() { pyxecutor = new Command(); @@ -61,7 +71,7 @@ void PLibForeFire::createDomain( int id void PLibForeFire::addLayer(char *type, char* layername, char* keyname){ - pyxecutor->getDomain()->addLayer(string(type),string(layername),string(keyname)); + requireDomain()->addLayer(string(type),string(layername),string(keyname)); } void PLibForeFire::setInt(char* name, int val){ @@ -102,7 +112,7 @@ bool PLibForeFire::isValued(char *name){ py::object PLibForeFire::getDataMatrixPy(char *name) { // Get the 2D data matrix using the string name. - std::vector> matrix = pyxecutor->getDomain()->getDataMatrix(string(name)); + std::vector> matrix = requireDomain()->getDataMatrix(string(name)); // Check if the matrix equals the sentinel value, i.e. a 1x1 matrix with -9999. if (matrix.size() == 1 && matrix[0].size() == 1 && matrix[0][0] == -9999) { @@ -144,7 +154,7 @@ void PLibForeFire::addScalarLayer(char *type, char *name, double x0 , double y0, size_t nk = nnz; size_t nl = nnl; - pyxecutor->getDomain()->addScalarLayer(type, lname, x0, y0, t0, width, height, timespan, ni, nj, nk, nl, values.mutable_data()); + requireDomain()->addScalarLayer(type, lname, x0, y0, t0, width, height, timespan, ni, nj, nk, nl, values.mutable_data()); } @@ -159,18 +169,18 @@ void PLibForeFire::addIndexLayer(char *type, char *name, double x0 , double y0, size_t nk = nnz; size_t nl = nnl; - pyxecutor->getDomain()->addIndexLayer(ltype, lname, x0, y0, t0, width, height, timespan, ni, nj, nk, nl, values.mutable_data()); + requireDomain()->addIndexLayer(ltype, lname, x0, y0, t0, width, height, timespan, ni, nj, nk, nl, values.mutable_data()); } py::array_t PLibForeFire::getDoubleArray(char* name){ - double lTime = pyxecutor->getDomain()->getSimulationTime(); + double lTime = requireDomain()->getSimulationTime(); return PLibForeFire::getDoubleArray(name, lTime); } py::array_t PLibForeFire::getDoubleArray(char* name, double t){ string lname(name); - FluxLayer* myFluxLayer = pyxecutor->getDomain()->getFluxLayer(lname); + FluxLayer* myFluxLayer = requireDomain()->getFluxLayer(lname); if ( myFluxLayer ){ FFArray* srcD; @@ -207,7 +217,7 @@ py::array_t PLibForeFire::getDoubleArray(char* name, double t){ return arr; } - DataLayer* myDataLayer = pyxecutor->getDomain()->getDataLayer(lname); + DataLayer* myDataLayer = requireDomain()->getDataLayer(lname); if ( myDataLayer ){ FFArray* srcD; @@ -377,7 +387,7 @@ PYBIND11_MODULE(_pyforefire, m) { } } else { // Parameter is not set: check for a data matrix with the given key. - std::vector> matrix = pyxecutor->getDomain()->getDataMatrix(const_cast(key.c_str())); + std::vector> matrix = requireDomain()->getDataMatrix(const_cast(key.c_str())); // If the returned matrix is the sentinel (1x1 with -9999), consider it non-existent. if (matrix.size() == 1 && matrix[0].size() == 1 && matrix[0][0] == -9999) { throw std::runtime_error("Parameter '" + key + "' does not exist."); diff --git a/tests/python/test_wheel.py b/tests/python/test_wheel.py index e82bf750..e8c307fa 100644 --- a/tests/python/test_wheel.py +++ b/tests/python/test_wheel.py @@ -49,6 +49,36 @@ def test_import(): return pyforefire +def test_no_domain(pyforefire): + """Calls that need a domain must raise before FireDomain[...], not segfault. + + Runs before test_simulation: the domain is process-wide, so once one + exists a new ForeFire() sees it too. + """ + import numpy as np + + ff = pyforefire.ForeFire() + calls = { + "ff[key]": lambda: ff["noSuchKey"], + "addLayer": lambda: ff.addLayer("propagation", "Iso", "propagationModel"), + "addScalarLayer": lambda: ff.addScalarLayer( + "data", "windU", 0, 0, 0, 10, 10, 0, np.zeros((1, 1, 2, 2)) + ), + "addIndexLayer": lambda: ff.addIndexLayer( + "table", "fuel", 0, 0, 0, 10, 10, 0, np.zeros((1, 1, 2, 2), dtype=np.int32) + ), + "getDoubleArray": lambda: ff.getDoubleArray("windU"), + } + for name, call in calls.items(): + try: + call() + except RuntimeError as e: + assert "FireDomain" in str(e), f"{name} raised an unrelated error: {e}" + else: + raise AssertionError(f"{name} did not raise without a FireDomain") + print(f"{len(calls)} calls raised cleanly without a FireDomain") + + def test_simulation(pyforefire): """Burn an isotropic circle and check the front actually grew.""" ff = pyforefire.ForeFire() @@ -86,6 +116,7 @@ def test_helpers(): def main(): pyforefire = test_import() + test_no_domain(pyforefire) test_simulation(pyforefire) test_helpers() print("\nOK: forefire wheel smoke test passed") From db9a6192953efeff85f5db6e4263d0b04d4446c0 Mon Sep 17 00:00:00 2001 From: Hugo <72709893+HugoFara@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:19:26 +0200 Subject: [PATCH 43/63] fix(core): stop the data broker freeing the domain's propagative layer (#206) Since #182 the domain deletes its propagative layer. loadData also registers that layer with the broker, which deleted every registered layer, so clear[] after loadData freed it twice. The broker now keeps it listed for lookup and leaves deleting it to the domain, in its destructor and when the layer's key is registered again. reload_case.ff now ends with clear[], so the runff step of the AddressSanitizer job covers this path. Fixes #205 --- CHANGELOG.md | 4 ++++ src/DataBroker.cpp | 9 ++++++--- tests/runff/reload_case.ff | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1a534c1..ade7e648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,9 @@ Merged since v2.5.0, not yet released. - `pyforefire` calls that read the domain (`ff[key]`, `addLayer`, `addScalarLayer`, `addIndexLayer`, `getDoubleArray`) segfaulted Python when run before `FireDomain[...]`. They now raise `RuntimeError`. ([#202]) +- `clear[]` after `loadData` freed the propagative layer twice and crashed. + The data broker lists that layer but no longer deletes it; the domain does. + ([#205]) - Rate of spread stayed finite and decreasing at high dead fuel moisture, and `DataBroker` no longer serves `moisture` through the five-slot getter, which returned a neighbouring property. ([#158]) @@ -222,3 +225,4 @@ repository stays easy to return to. [#196]: https://github.com/forefireAPI/forefire/issues/196 [#202]: https://github.com/forefireAPI/forefire/pull/202 [#199]: https://github.com/forefireAPI/forefire/pull/199 +[#205]: https://github.com/forefireAPI/forefire/issues/205 diff --git a/src/DataBroker.cpp b/src/DataBroker.cpp index b67cd5f8..bf89c961 100644 --- a/src/DataBroker.cpp +++ b/src/DataBroker.cpp @@ -42,10 +42,12 @@ namespace libforefire DataBroker::~DataBroker() { - // Deleting layers + // Deleting layers. loadData lists the domain's propagative layer here + // too, so it can be looked up, but the domain frees that one (#205). while (!layers.empty()) { - delete layers.back(); + if (layers.back() != domain->getPropagativeLayer()) + delete layers.back(); layers.pop_back(); } // Deleting atmospheric and parallel data @@ -324,7 +326,8 @@ namespace libforefire DataLayer *oldlayer = ilayer->second; layersMap.erase(name); layers.remove(oldlayer); - delete oldlayer; + if (oldlayer != domain->getPropagativeLayer()) + delete oldlayer; //} } diff --git a/tests/runff/reload_case.ff b/tests/runff/reload_case.ff index 00e5c874..1a10beef 100644 --- a/tests/runff/reload_case.ff +++ b/tests/runff/reload_case.ff @@ -3,4 +3,5 @@ loadData[data.nc;2025-02-10T17:35:54Z] include[to_reload.ff] step[dt=1200] setParameter[dumpMode=kml] -print[real_case.kml] \ No newline at end of file +print[real_case.kml] +clear[] From b496272a91c4f8214d61dd5a10a30493faadd7ec Mon Sep 17 00:00:00 2001 From: Hugo <72709893+HugoFara@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:34:07 +0200 Subject: [PATCH 44/63] docs(changelog): fill in [Unreleased] ahead of v2.6.0 (#207) * docs(changelog): correct the v2.5.0 wheel platforms PyPI has three wheels for 2.5.0 -- manylinux x86_64, manylinux aarch64 and macosx_14_0_arm64. There is no Intel macOS wheel: the macos-13 runner was retired and its job dropped in e43e0a0, which is an ancestor of the tag. * docs(changelog): add the entries merged since v2.5.0 without one #179, #181, #182, #189, #190, #192 and #197 had no entry, and the listenHTTP entry did not credit #203. Version bumps, merges and the documentation backports are left out. --------- Co-authored-by: Jean-Baptiste Filippi --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ade7e648..e48e8525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,15 @@ Merged since v2.5.0, not yet released. process, so several simulations in one Python process no longer share state, and a crash in the kernel raises `pyforefire.WorkerDied` instead of ending Python. `ForeFire()` itself is unchanged. ([#175]) +- `forefire-validate landscape.nc fuels.csv` checks a landscape against its + fuel table before a run: fuel indices missing from the table, a fuel and + elevation shape mismatch, and missing elevation or wind. ([#190]) +- `tools/preprocessing/genForeFireCase.py` is back as a general landscape + writer. Its 3-D and 4-D fields are written in the right axis order, it uses + `netCDF4` instead of the deprecated `scipy.io.netcdf`, and it checks the + required `parametersProperties` keys before writing. ([#179]) +- A User Guide page on driving a simulation from NumPy arrays, with no + landscape file and no GIS. ([#189]) ### Fixed @@ -93,17 +102,23 @@ Merged since v2.5.0, not yet released. `systemExec` is refused over HTTP, the static file server only serves the run directory and `FOREFIREHOME/tools/htdocs`, and commands from another site or host name are refused. Other commands can still read and write files, so the - server is meant for trusted local use. ([#199]) + server is meant for trusted local use. ([#199], [#203]) +- A safe-topology failure in a step no longer ends the process; the command + reports an error and the coupled run continues. ([#181]) +- `stb_image_write.h` moved to `third_party/`, with its version, origin and + MIT / public-domain licence recorded. ([#192]) ## [v2.5.0] — 2026-08-11 ### Added -- **Pip-installable wheels** for Linux (x86_64, aarch64) and macOS (Apple - Silicon and Intel), CPython 3.9 and newer. `pip install forefire` gives both - the `forefire` command-line interpreter and the `pyforefire` module, with - NetCDF bundled inside the wheel. Wheels are built without MPI and without - `-march=native`. ([#151]) +- **Pip-installable wheels** for Linux (x86_64, aarch64) and macOS on Apple + Silicon, CPython 3.9 and newer. `pip install forefire` gives both the + `forefire` command-line interpreter and the `pyforefire` module, with NetCDF + bundled inside the wheel. Wheels are built without MPI and without + `-march=native`. There is no Intel macOS wheel — the `macos-13` runner was + retired before the release, so Intel Macs build from the source + distribution. ([#151]) ### Changed @@ -225,4 +240,11 @@ repository stays easy to return to. [#196]: https://github.com/forefireAPI/forefire/issues/196 [#202]: https://github.com/forefireAPI/forefire/pull/202 [#199]: https://github.com/forefireAPI/forefire/pull/199 +[#179]: https://github.com/forefireAPI/forefire/pull/179 +[#181]: https://github.com/forefireAPI/forefire/pull/181 +[#182]: https://github.com/forefireAPI/forefire/pull/182 +[#189]: https://github.com/forefireAPI/forefire/pull/189 +[#190]: https://github.com/forefireAPI/forefire/pull/190 +[#192]: https://github.com/forefireAPI/forefire/pull/192 +[#203]: https://github.com/forefireAPI/forefire/pull/203 [#205]: https://github.com/forefireAPI/forefire/issues/205 From 15ef907f90c40f488cbc9e9012de1d624f8d2aa3 Mon Sep 17 00:00:00 2001 From: Antonio Leblanc <56365887+antonio-leblanc@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:47:40 -0300 Subject: [PATCH 45/63] fix(core): addIndexLayer returns true when it registers a fuel table (#209) The "table" branch registered the FuelDataLayer and then fell through to the trailing `return false`. Nothing in the engine reads the result, only the bindings do, so the layer worked while reporting it had not been made. Co-authored-by: HugoFara --- src/FireDomain.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FireDomain.cpp b/src/FireDomain.cpp index a10fde54..6c56c5df 100644 --- a/src/FireDomain.cpp +++ b/src/FireDomain.cpp @@ -836,6 +836,7 @@ FuelDataLayer* newlayer = new FuelDataLayer(name, origin, t0, span, timespan, nnx, nny, nnz, nnk, values); dataBroker->registerLayer(name, newlayer); + return true; } return false; From 77c31072c13906698011220bb426c31e9aea05ef Mon Sep 17 00:00:00 2001 From: Antonio Leblanc <56365887+antonio-leblanc@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:50:22 -0300 Subject: [PATCH 46/63] feat(build): FOREFIRE_ENABLE_NETCDF to build without NetCDF (#208) * feat(build): FOREFIRE_ENABLE_NETCDF to build without NetCDF `#include ` in DataBroker.h pulled NetCDF into 48 of the 58 files in src/. A new FF_NO_NETCDF guard compiles out every NetCDF-backed path (loadFromNCFile, load/saveArrivalTimeNC, saveData, writeNetCDF, the PGD form of createDomain, the NetCDF form of loadData). Each keeps a stub that prints that the build has no NetCDF, so the command table is unchanged. Default is ON: a normal build compiles exactly the same code as before (tests/runff KML and NetCDF outputs match their references). Only adds lines, no existing line changes. Needed for a WebAssembly build (#171), and allows a slim pip wheel without NetCDF/HDF5. * docs(changelog): FOREFIRE_ENABLE_NETCDF --------- Co-authored-by: HugoFara --- CHANGELOG.md | 5 +++++ CMakeLists.txt | 8 +++++++- src/Command.cpp | 29 +++++++++++++++++++++++++++++ src/DataBroker.cpp | 15 +++++++++++++++ src/DataBroker.h | 8 ++++++++ src/FireDomain.cpp | 16 ++++++++++++++++ 6 files changed, 80 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e48e8525..36a0d091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,10 @@ Merged since v2.5.0, not yet released. required `parametersProperties` keys before writing. ([#179]) - A User Guide page on driving a simulation from NumPy arrays, with no landscape file and no GIS. ([#189]) +- `-DFOREFIRE_ENABLE_NETCDF=OFF` builds the engine without NetCDF or HDF5, + the first step towards a WebAssembly build and a slim wheel. Commands that + read or write NetCDF files report that the build has no NetCDF support. + The default stays on. ([#171]) ### Fixed @@ -248,3 +252,4 @@ repository stays easy to return to. [#192]: https://github.com/forefireAPI/forefire/pull/192 [#203]: https://github.com/forefireAPI/forefire/pull/203 [#205]: https://github.com/forefireAPI/forefire/issues/205 +[#171]: https://github.com/forefireAPI/forefire/issues/171 diff --git a/CMakeLists.txt b/CMakeLists.txt index 798f6e38..1b4f3d2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ else() endif() option(FOREFIRE_ENABLE_MPI "Enable MPI coupling when MPI is available" ${_ff_default_mpi}) +option(FOREFIRE_ENABLE_NETCDF "Build the NetCDF-backed I/O paths" ON) option(FOREFIRE_NATIVE_ARCH "Optimise for the build machine's CPU (-march=native)" ${_ff_default_native}) option(FOREFIRE_BUILD_PYTHON "Build the pyforefire Python extension module" ${_ff_default_python}) option(FOREFIRE_STATIC_CORE "Build the ForeFire core as a static library" ${_ff_default_static}) @@ -137,7 +138,12 @@ endif() # detection logic below lives in exactly one place. add_library(forefire_netcdf INTERFACE) -if(DEFINED ENV{SRC_MESONH} AND DEFINED ENV{XYZ} AND DEFINED ENV{FF_STATIC}) +if(NOT FOREFIRE_ENABLE_NETCDF) + # FF_NO_NETCDF compiles out every NetCDF-backed path; each entry point keeps + # a stub that reports the missing support (see DataBroker.h). + message(STATUS "NetCDF support disabled (FOREFIRE_ENABLE_NETCDF=OFF)") + target_compile_definitions(forefire_netcdf INTERFACE FF_NO_NETCDF) +elseif(DEFINED ENV{SRC_MESONH} AND DEFINED ENV{XYZ} AND DEFINED ENV{FF_STATIC}) message(STATUS "MESONH and XYZ environment variables are set. FF_STATIC too, static NetCDF.") set(MESONH $ENV{SRC_MESONH}) set(XYZ $ENV{XYZ}) diff --git a/src/Command.cpp b/src/Command.cpp index f6913284..56352509 100644 --- a/src/Command.cpp +++ b/src/Command.cpp @@ -89,6 +89,9 @@ namespace libforefire { size_t n = argCount(arg); +#ifndef FF_NO_NETCDF + // The two-argument form reads the domain extent out of a MesoNH PGD + // file, so it exists only where NetCDF does. if (n == 2) { @@ -176,6 +179,7 @@ namespace libforefire } } +#endif /* FF_NO_NETCDF */ if (n >= 3) { FFPoint SW = getPoint("sw", arg); @@ -2502,6 +2506,18 @@ namespace libforefire return normal; } +#ifdef FF_NO_NETCDF + + // saveData writes a landscape file, which is NetCDF end to end; there is no + // partial behaviour to keep when NetCDF is compiled out. + int Command::saveData(const std::string &arg, size_t &numTabs) + { + std::cout << "saveData: this build has no NetCDF support" << std::endl; + return error; + } + +#else + int Command::saveData(const std::string &arg, size_t &numTabs) { // Check for an argument @@ -2836,6 +2852,8 @@ namespace libforefire } } +#endif /* FF_NO_NETCDF */ + int Command::loadData(const string &arg, size_t &numTabs) { @@ -2865,6 +2883,11 @@ namespace libforefire if (args.size() == 2) { +#ifdef FF_NO_NETCDF + cout << "loadData: reading a domain from " << path + << " needs NetCDF, which this build has no support for" << endl; + return error; +#else simParam->setParameter("NetCDFfile", args[0]); try { @@ -2950,6 +2973,7 @@ namespace libforefire } ExecuteCommand(com); +#endif /* FF_NO_NETCDF */ } if (args.size() == 1) { @@ -3710,6 +3734,10 @@ namespace libforefire void Command::writeNetCDF(const char *filename, const string &varName, const std::vector> &matrix, const vector &latitudes, const vector &longitudes) { +#ifdef FF_NO_NETCDF + std::cerr << "writeNetCDF: this build has no NetCDF support, " + << filename << " not written" << std::endl; +#else try { // Create (or replace) the NetCDF file using NetCDF-4 mode. @@ -3753,6 +3781,7 @@ namespace libforefire { std::cerr << "Error writing NetCDF file: " << e.what() << std::endl; } +#endif /* FF_NO_NETCDF */ } void Command::writeASCII(const char *filename, const std::vector> &matrix, double SWX, double SWY, double NEX, double NEY) { diff --git a/src/DataBroker.cpp b/src/DataBroker.cpp index bf89c961..0413490d 100644 --- a/src/DataBroker.cpp +++ b/src/DataBroker.cpp @@ -1105,6 +1105,19 @@ namespace libforefire } } +#ifdef FF_NO_NETCDF + + void DataBroker::loadFromNCFile(std::string filename) + { + // The empty-filename early return matches the NetCDF path: the command + // layer calls this with no file on every session setup. + if (filename.empty()) return; + cout << "loadFromNCFile: this build has no NetCDF support, ignoring " + << filename << endl; + } + +#else + void DataBroker::loadFromNCFile(std::string filename) { if(filename.empty()) @@ -1682,4 +1695,6 @@ namespace libforefire return data; } +#endif /* FF_NO_NETCDF */ + } diff --git a/src/DataBroker.h b/src/DataBroker.h index b54c0281..2c3ab066 100644 --- a/src/DataBroker.h +++ b/src/DataBroker.h @@ -22,9 +22,15 @@ #include "FFArrays.h" #include "ParallelData.h" #include "SimulationParameters.h" +// FF_NO_NETCDF drops every NetCDF-backed code path. The umbrella header is +// included in exactly this one place, and the `using namespace` below drags it +// into every translation unit that reaches DataBroker.h, so the guard here is +// what makes a NetCDF-free build (Emscripten, slim wheels) possible at all. +#ifndef FF_NO_NETCDF #include using namespace netCDF; using namespace netCDF::exceptions; +#endif using namespace std; @@ -205,6 +211,7 @@ class DataBroker { bool isRelevantData(FFPoint&, FFPoint&); +#ifndef FF_NO_NETCDF /*! \brief loading a NCXYZTDataLayer from an NcFile */ XYZTDataLayer* constructXYZTLayer(NcVar&, FFPoint&, FFPoint&,double , double , int ); /*! \brief loading a FuelDataLayer from an NcFile */ @@ -216,6 +223,7 @@ class DataBroker { /*! \transpose data from fortran netcdf*/ double* readAndTransposeFortranProjectedField(NcVar* , const size_t& ,const size_t& , const size_t& ,const size_t& ,bool , int ); int* readAndTransposeIntFortranProjectedField(NcVar* , const size_t& ,const size_t& , const size_t& ,const size_t&, bool , int ); +#endif public: diff --git a/src/FireDomain.cpp b/src/FireDomain.cpp index 6c56c5df..e544a42a 100644 --- a/src/FireDomain.cpp +++ b/src/FireDomain.cpp @@ -3461,6 +3461,20 @@ +#ifdef FF_NO_NETCDF + + void FireDomain::loadArrivalTimeNC(string fname){ + cout << "loadArrivalTimeNC: this build has no NetCDF support, ignoring " + << fname << endl; + } + + void FireDomain::saveArrivalTimeNC(){ + cout << "saveArrivalTimeNC: this build has no NetCDF support, " + "nothing written" << endl; + } + +#else + void FireDomain::loadArrivalTimeNC(string fname){ if (getDomainID()!=0) return; @@ -3599,6 +3613,8 @@ } +#endif /* FF_NO_NETCDF */ + void FireDomain::visualizeBurningMatrixAroundNode(FireNode* fn){ if( !striclyWithinDomain(fn) ) return; /* Position of the firenode in the burning matrix */ From 32b9834f189c44aeb00458b4ee1dd0584dc1da82 Mon Sep 17 00:00:00 2001 From: Hugo <72709893+HugoFara@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:43:38 +0200 Subject: [PATCH 47/63] feat(build): compile the engine to WebAssembly (#210) * feat(build): compile the engine to WebAssembly Builds on FOREFIRE_ENABLE_NETCDF (#208) for #171. emcmake now configures the WASM build with no arguments: EMSCRIPTEN turns off NetCDF, MPI, the Python module, the unit tests and the termios-driven CLI shell, and turns on an embind module that mirrors the pybind11 API with typed arrays in place of NumPy arrays. The binding transposes C-order arrays into the x-major order the data layers index with, as _pyforefire.cpp does. The module links to a ~1 MB .wasm and runs: bindings/wasm/smoke.mjs reproduces the NumPy documentation's array-only case and gets the same front a native pyforefire does, 349 nodes. * ci(wasm): build the module and run the smoke test under Node --- .github/workflows/wasm.yml | 65 +++++++++ CHANGELOG.md | 5 + CMakeLists.txt | 82 +++++++++++- bindings/wasm/README.md | 110 ++++++++++++++++ bindings/wasm/forefire_wasm.cpp | 226 ++++++++++++++++++++++++++++++++ bindings/wasm/smoke.mjs | 115 ++++++++++++++++ 6 files changed, 597 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/wasm.yml create mode 100644 bindings/wasm/README.md create mode 100644 bindings/wasm/forefire_wasm.cpp create mode 100644 bindings/wasm/smoke.mjs diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml new file mode 100644 index 00000000..05f7428f --- /dev/null +++ b/.github/workflows/wasm.yml @@ -0,0 +1,65 @@ +name: 🌐 Build WASM + +# Builds the WebAssembly module (issue #171) and runs the Node smoke test. The +# .wasm is built here rather than committed: it is a megabyte of generated +# output, and a stale binary next to live sources is worse than a two minute +# build. + +on: + push: + branches: + - "master" + - "dev" + pull_request: + branches: [ "master", "dev" ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-wasm: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + # No LFS: the WASM build reads no test data, and emcmake turns the + # integrity check off by default. + + - name: Cache Emscripten SDK + id: emsdk-cache + uses: actions/cache@v4 + with: + path: ~/emsdk + key: emsdk-${{ runner.os }}-6.0.6 + + - name: Install Emscripten + if: steps.emsdk-cache.outputs.cache-hit != 'true' + run: | + # Pinned to the version the module was developed and tested against. + git clone --depth 1 https://github.com/emscripten-core/emsdk.git ~/emsdk + ~/emsdk/emsdk install 6.0.6 + ~/emsdk/emsdk activate 6.0.6 + + - name: Build the WebAssembly module + run: | + source ~/emsdk/emsdk_env.sh + emcmake cmake -S . -B build-wasm -DCMAKE_BUILD_TYPE=Release + cmake --build build-wasm -j"$(nproc)" + + - name: Report size + run: | + { + echo "### WebAssembly build" + echo "| file | raw | gzip | brotli |" + echo "| --- | --- | --- | --- |" + for f in bin/forefire.wasm bin/forefire.mjs; do + printf '| `%s` | %s | %s | %s |\n' "$(basename "$f")" \ + "$(stat -c%s "$f" | numfmt --to=iec)" \ + "$(gzip -c "$f" | wc -c | numfmt --to=iec)" \ + "$(brotli -c "$f" | wc -c | numfmt --to=iec)" + done + } | tee -a "$GITHUB_STEP_SUMMARY" + + - name: Smoke test under Node + run: node bindings/wasm/smoke.mjs bin/forefire.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a0d091..f6769e36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,11 @@ Merged since v2.5.0, not yet released. the first step towards a WebAssembly build and a slim wheel. Commands that read or write NetCDF files report that the build has no NetCDF support. The default stays on. ([#171]) +- An experimental WebAssembly build: `emcmake cmake` compiles the engine + without NetCDF into an ES module whose embind API mirrors `pyforefire`, with + typed arrays in place of NumPy arrays. `FOREFIRE_BUILD_CLI` and + `FOREFIRE_BUILD_WASM` control the two executables. See + `bindings/wasm/README.md`. ([#171]) ### Fixed diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b4f3d2d..871674c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,8 +43,28 @@ else() set(_ff_default_tests ON) endif() +# Emscripten has no NetCDF and no reason for one: a browser build reads its +# data from JS typed arrays. Detecting it here rather than making the caller +# pass the flags keeps `emcmake cmake` a one-liner. +if(EMSCRIPTEN) + set(_ff_default_mpi OFF) + set(_ff_default_native OFF) + set(_ff_default_python OFF) + set(_ff_default_static ON) + set(_ff_default_tools OFF) + set(_ff_default_lfs OFF) + set(_ff_default_netcdf OFF) + set(_ff_default_cli OFF) + set(_ff_default_tests OFF) +else() + set(_ff_default_netcdf ON) + set(_ff_default_cli ON) +endif() + option(FOREFIRE_ENABLE_MPI "Enable MPI coupling when MPI is available" ${_ff_default_mpi}) -option(FOREFIRE_ENABLE_NETCDF "Build the NetCDF-backed I/O paths" ON) +option(FOREFIRE_ENABLE_NETCDF "Build the NetCDF-backed I/O paths" ${_ff_default_netcdf}) +option(FOREFIRE_BUILD_CLI "Build the interactive forefire command line shell" ${_ff_default_cli}) +option(FOREFIRE_BUILD_WASM "Build the Emscripten/embind module" ${EMSCRIPTEN}) option(FOREFIRE_NATIVE_ARCH "Optimise for the build machine's CPU (-march=native)" ${_ff_default_native}) option(FOREFIRE_BUILD_PYTHON "Build the pyforefire Python extension module" ${_ff_default_python}) option(FOREFIRE_STATIC_CORE "Build the ForeFire core as a static library" ${_ff_default_static}) @@ -270,6 +290,12 @@ if(FOREFIRE_WARNINGS_AS_ERRORS AND _ff_warning_flags) endif() endif() +if(EMSCRIPTEN) + # Emscripten defaults to -fno-exceptions; the command parser signals bad + # input by throwing, so every translation unit needs them enabled. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexceptions") +endif() + # ---------------------------------- # Output Directories # ---------------------------------- @@ -330,10 +356,14 @@ endif() # ---------------------------------- # Main Project Executable # ---------------------------------- -add_executable(forefire app/forefire/ForeFire.cpp app/forefire/AdvancedLineEditor.cpp) -target_link_libraries(forefire PRIVATE forefireL) -if(_ff_warning_flags) - target_compile_options(forefire PRIVATE ${_ff_warning_flags}) +# The shell is termios-driven and has no meaning in a browser, so a WASM build +# skips it and ships the embind module below instead. +if(FOREFIRE_BUILD_CLI) + add_executable(forefire app/forefire/ForeFire.cpp app/forefire/AdvancedLineEditor.cpp) + target_link_libraries(forefire PRIVATE forefireL) + if(_ff_warning_flags) + target_compile_options(forefire PRIVATE ${_ff_warning_flags}) + endif() endif() # ---------------------------------- @@ -344,6 +374,44 @@ if(FOREFIRE_BUILD_TESTS) add_subdirectory(tests/unit) endif() +# ---------------------------------- +# WebAssembly Module +# ---------------------------------- +if(FOREFIRE_BUILD_WASM) + add_executable(forefire_wasm bindings/wasm/forefire_wasm.cpp) + target_link_libraries(forefire_wasm PRIVATE forefireL) + # Two things are non-obvious here. embind refuses to compile below C++17, + # so this one translation unit moves up while the core stays on C++11 — + # both sit on the same libc++. And the output is an ES module, which Node + # only recognises from a .js file when a package.json says so, hence .mjs. + set_target_properties(forefire_wasm PROPERTIES + OUTPUT_NAME "forefire" + SUFFIX ".mjs" + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON) + target_link_options(forefire_wasm PRIVATE + "--bind" + # A single ES module plus its .wasm, so a static page can import it. + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=createForeFire" + "-sENVIRONMENT=web,worker,node" + # The domain allocates its grids up front and can grow well past the + # 16 MB default; the fire domain sizes are not known at build time. + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=64MB" + "-sSTACK_SIZE=4MB" + # C++ exceptions are load-bearing here: BadOption and friends are how + # the command parser reports bad input. + "-fexceptions" + # There is no main(): the module is driven entirely through embind. + # FS stays exported so callers can stage fuels.csv and read back what + # print[] writes. + "-sEXPORTED_RUNTIME_METHODS=['FS']" + "-sFORCE_FILESYSTEM=1") + target_compile_options(forefire_wasm PRIVATE "-fexceptions") +endif() + # ---------------------------------- # Python Extension Module # ---------------------------------- @@ -386,7 +454,9 @@ if(NOT _ff_wheel_build) set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE") # Adjust as necessary # Specify installation directories - install(TARGETS forefire RUNTIME DESTINATION bin) + if(FOREFIRE_BUILD_CLI) + install(TARGETS forefire RUNTIME DESTINATION bin) + endif() if(NOT FOREFIRE_STATIC_CORE) # With a static core there is no libforefireL to install: it is linked # straight into the executables. diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md new file mode 100644 index 00000000..57eb87c0 --- /dev/null +++ b/bindings/wasm/README.md @@ -0,0 +1,110 @@ +# ForeFire in WebAssembly + +Follow-up to [issue #171](https://github.com/forefireAPI/forefire/issues/171): +compile the engine to WebAssembly and run a simulation with no NetCDF, in Node +or in a browser tab. + +This is an experimental build, not a supported one yet. It answers the three +questions the issue left open: does it link, does it run, how big is it. + +`.github/workflows/wasm.yml` builds the module and runs `smoke.mjs` on every +pull request. The `.wasm` is built in CI rather than committed: it is a +megabyte of generated output, and a stale binary sitting next to live sources +is worse than a two minute build. + +## What made it possible + +`#include ` sat in `src/DataBroker.h`, and the `using namespace netCDF` +next to it pulled the dependency into 48 of the 58 files in `src/`. +`-DFOREFIRE_ENABLE_NETCDF=OFF` (#208) compiles that include out, along with +every code path behind it, and leaves a stub in each entry point that reports +the missing support. The command table is unchanged, so a script that reaches +for NetCDF gets a message instead of a link error. + +Nothing else needed touching. MPI was already behind `MPI_COUPLING`, the HTTP +server and `popen` are never reached from the WASM entry point, and `termios` +lives only in the CLI shell, which a WASM build does not compile. + +## Building + +Needs Emscripten. With the SDK image: + +```sh +podman run --rm -v "$PWD":/w:z -w /w docker.io/emscripten/emsdk:6.0.6 bash -lc ' + emcmake cmake -S . -B build-wasm -DCMAKE_BUILD_TYPE=Release && + cmake --build build-wasm -j8' +``` + +`emcmake` sets `EMSCRIPTEN`, which turns off NetCDF, MPI, the Python module, +the unit tests and the CLI shell, and turns on `forefire_wasm`. The outputs +land in `bin/`: + +| File | Size | gzip | brotli | +| --- | --- | --- | --- | +| `forefire.wasm` | 990 KB | 298 KB | 224 KB | +| `forefire.mjs` | 131 KB | 33 KB | — | + +Roughly 257 KB over the wire with brotli, for the whole engine and all its +propagation and flux models. + +## Running + +Headless, under Node: + +```sh +node bindings/wasm/smoke.mjs bin/forefire.mjs +``` + +## The API + +`forefire.mjs` is an ES module exporting a `createForeFire()` factory. It +mirrors the pybind11 module in `bindings/python`, with typed arrays in place of +NumPy arrays — see +[Running from NumPy arrays](../../docs/source/user_guide/python_arrays.rst) for +the concepts, which carry over unchanged. + +```js +import createForeFire from "./forefire.mjs"; + +const Module = await createForeFire(); +const ff = new Module.ForeFire(); + +ff.setString("propagationModel", "WindDriven"); +ff.execute("FireDomain[sw=(0,0,0);ne=(4000,4000,0);t=0]"); +ff.addLayer("propagation", "WindDriven", "propagationModel"); +ff.addIndexLayer("table", "fuel", 0, 0, 0, 4000, 4000, 0, 200, 200, 1, 1, fuelArray); +ff.execute("startFire[loc=(2000,2000,0.);t=0]"); +ff.execute("step[dt=30]"); +const front = ff.execute("print[]"); +``` + +The layer methods take the four dimensions explicitly — `nx, ny, nz, nt` — where +the NumPy binding reads them off the array's shape. Otherwise the contract is +identical: pass a flat C-order `(t, z, y, x)` array, indexed +`x + nx * (y + ny * (z + nz * t))`, exactly as `numpy.ndarray.ravel()` would +give you. The binding transposes it into the x-major order the data layers +index with, the same shuffle `_pyforefire.cpp` performs. + +## What was measured + +- **All 58 files in `src/` compile** under `em++` with + `FOREFIRE_ENABLE_NETCDF=OFF`. Zero errors. The only diagnostics are seven + `-Wvla-cxx-extension` warnings, the latent portability issue #171 already + flags. +- **It links and runs.** `smoke.mjs` runs a full 240 s simulation. +- **The answers match the Python binding.** `smoke.mjs` is a line-for-line port + of the worked example in the NumPy documentation. Run against a native + `pyforefire` built from the same tree, the two agree exactly: 349 front nodes, + x 1980..3905, y 1763..2308, the front stretched downwind as the doc describes. + +## Known limits + +- **Single-threaded.** Emscripten pthreads need `SharedArrayBuffer` and + COOP/COEP headers, which is exactly the deployment complexity a static page + is meant to avoid. Nothing in the engine's simulation path threads today — + only the HTTP server does, and it is not reachable here. +- **The embind translation unit is C++17**; the core stays C++11. embind + refuses to compile below C++17, and mixing is fine against one libc++. +- **No file I/O worth the name.** `FS` is exported so a caller can stage + `fuels.csv` or read back what `print[]` writes, but there is no landscape + file support at all — arrays are the only way in. diff --git a/bindings/wasm/forefire_wasm.cpp b/bindings/wasm/forefire_wasm.cpp new file mode 100644 index 00000000..dc403a1f --- /dev/null +++ b/bindings/wasm/forefire_wasm.cpp @@ -0,0 +1,226 @@ +/** + * @file forefire_wasm.cpp + * @brief Emscripten/embind entry point for the browser build. + * + * A spike for issue #171. The shape deliberately mirrors the pybind11 module + * in bindings/python: one Command per instance, commands driven as strings, + * gridded inputs handed over as typed arrays instead of NumPy arrays. + * + * Everything here assumes FF_NO_NETCDF, so there is no file-backed landscape + * loading: the caller builds the domain and its layers from JS. + */ + +#include +#include + +#include +#include +#include + +#include "Command.h" +#include "FireDomain.h" +#include "include/Version.h" + +using namespace libforefire; + +namespace { + +/*! \brief Copy a JS TypedArray into a contiguous C++ vector. */ +template +std::vector toVector(const emscripten::val &array) { + const size_t length = array["length"].as(); + std::vector out(length); + if (length == 0) return out; + // A view over the heap slice the vector already owns; the JS side does the + // copy, which avoids a per-element val round trip. + emscripten::val heap = emscripten::val(emscripten::typed_memory_view(length, out.data())); + heap.call("set", array); + return out; +} + +/*! \brief Transpose a (t,z,y,x) C-order buffer into the x-major order the data + * layers index with. + * + * XYZTDataLayer and FuelDataLayer read their values as + * `x*(ny*nz*nt) + y*(nz*nt) + z*nt + t`, not as the row-major layout a caller + * naturally builds. The pybind11 binding does this same shuffle before + * handing NumPy data over; skipping it silently transposes the field, which + * for a two-plane wind layer swaps the u and v responses and sends the fire + * off at ninety degrees to the wind. + */ +template +std::vector toLayerOrder(const std::vector &src, size_t nx, size_t ny, + size_t nz, size_t nt) { + std::vector out(src.size()); + for (size_t it = 0; it < nt; ++it) + for (size_t iz = 0; iz < nz; ++iz) + for (size_t iy = 0; iy < ny; ++iy) + for (size_t ix = 0; ix < nx; ++ix) { + const size_t from = ix + nx * (iy + ny * (iz + nz * it)); + const size_t to = ix * (ny * nz * nt) + iy * (nz * nt) + iz * nt + it; + out[to] = src[from]; + } + return out; +} + +class ForeFireWasm { + Command executor; + +public: + ForeFireWasm() : executor() {} + + /*! \brief Run one ForeFire command, returning whatever it printed. */ + std::string execute(const std::string &command) { + std::ostringstream captured; + executor.setOstringstream(&captured); + std::string cmd(command); + executor.ExecuteCommand(cmd); + return captured.str(); + } + + /*! \brief Current front geometry, in the shell's own dump format. */ + std::string dumpString() { return executor.dumpString(); } + + double getTime() { return executor.getTime(); } + + void setInt(const std::string &name, int value) { + executor.currentSession.params->setInt(name, value); + } + int getInt(const std::string &name) { + return executor.currentSession.params->getInt(name); + } + void setDouble(const std::string &name, double value) { + executor.currentSession.params->setDouble(name, value); + } + double getDouble(const std::string &name) { + return executor.currentSession.params->getDouble(name); + } + void setString(const std::string &name, const std::string &value) { + executor.currentSession.params->setParameter(name, value); + } + std::string getString(const std::string &name) { + return executor.currentSession.params->getParameter(name); + } + + /*! \brief Attach a model-backed layer, e.g. ("propagation", "WindDriven", + * "propagationModel"). Distinct from the addLayer *command*, which + * builds constant layers instead. */ + void addLayer(const std::string &type, const std::string &name, + const std::string &key) { + FireDomain *domain = executor.getDomain(); + if (domain == 0) return; + domain->addLayer(type, name, key); + } + + /*! \brief Register a continuous field (altitude, wind, moisture, ...). */ + bool addScalarLayer(const std::string &type, const std::string &name, + double x0, double y0, double t0, + double width, double height, double timespan, + int nx, int ny, int nz, int nt, + const emscripten::val &values) { + FireDomain *domain = executor.getDomain(); + if (domain == 0) return false; + std::vector data = toVector(values); + size_t nnx = nx, nny = ny, nnz = nz, nnt = nt; + if (data.size() != nnx * nny * nnz * nnt) return false; + data = toLayerOrder(data, nnx, nny, nnz, nnt); + // addScalarLayer keeps the pointer, so the layer needs storage that + // outlives this call; the domain owns it from here on. + double *owned = new double[data.size()]; + std::copy(data.begin(), data.end(), owned); + return domain->addScalarLayer(type, name, x0, y0, t0, width, height, + timespan, nnx, nny, nnz, nnt, owned); + } + + /*! \brief Register an index field (fuel map, flux model map). */ + bool addIndexLayer(const std::string &type, const std::string &name, + double x0, double y0, double t0, + double width, double height, double timespan, + int nx, int ny, int nz, int nt, + const emscripten::val &values) { + FireDomain *domain = executor.getDomain(); + if (domain == 0) return false; + std::vector data = toVector(values); + size_t nnx = nx, nny = ny, nnz = nz, nnt = nt; + if (data.size() != nnx * nny * nnz * nnt) return false; + data = toLayerOrder(data, nnx, nny, nnz, nnt); + int *owned = new int[data.size()]; + std::copy(data.begin(), data.end(), owned); + return domain->addIndexLayer(type, name, x0, y0, t0, width, height, + timespan, nnx, nny, nnz, nnt, owned); + } + + /*! \brief Read a layer back as {shape:[nt,nz,ny,nx], data:Float64Array}. */ + emscripten::val getDoubleArray(const std::string &name) { + FireDomain *domain = executor.getDomain(); + if (domain == 0) return emscripten::val::null(); + const double t = domain->getSimulationTime(); + + FFArray *src = 0; + FluxLayer *fluxLayer = domain->getFluxLayer(name); + if (fluxLayer != 0) { + fluxLayer->getMatrix(&src, t); + } else { + DataLayer *dataLayer = domain->getDataLayer(name); + if (dataLayer == 0) return emscripten::val::null(); + dataLayer->getMatrix(&src, t); + } + if (src == 0) return emscripten::val::null(); + + const size_t nnx = src->getDim("x"); + const size_t nny = src->getDim("y"); + const size_t nnz = src->getDim("z"); + const size_t nnt = src->getDim("t"); + const double *data = src->getData(); + + // Same Fortran-to-C reshuffle the NumPy binding does, so the JS view + // indexes as [t][z][y][x]. + std::vector reshaped(nnx * nny * nnz * nnt); + for (size_t it = 0; it < nnt; ++it) + for (size_t iz = 0; iz < nnz; ++iz) + for (size_t iy = 0; iy < nny; ++iy) + for (size_t ix = 0; ix < nnx; ++ix) { + const size_t cIndex = ix + nnx * (iy + nny * (iz + nnz * it)); + const size_t fIndex = it + nnt * (iz + nnz * (iy + nny * ix)); + reshaped[cIndex] = data[fIndex]; + } + + emscripten::val shape = emscripten::val::array(); + shape.call("push", nnt); + shape.call("push", nnz); + shape.call("push", nny); + shape.call("push", nnx); + + emscripten::val out = emscripten::val::object(); + out.set("shape", shape); + // slice() forces a copy out of the heap view, which would otherwise + // dangle the moment `reshaped` goes out of scope or the heap grows. + out.set("data", emscripten::val(emscripten::typed_memory_view( + reshaped.size(), reshaped.data())) + .call("slice")); + return out; + } +}; + +std::string version() { return std::string(ff_version); } + +} // namespace + +EMSCRIPTEN_BINDINGS(forefire) { + emscripten::function("version", &version); + emscripten::class_("ForeFire") + .constructor<>() + .function("execute", &ForeFireWasm::execute) + .function("dumpString", &ForeFireWasm::dumpString) + .function("getTime", &ForeFireWasm::getTime) + .function("setInt", &ForeFireWasm::setInt) + .function("getInt", &ForeFireWasm::getInt) + .function("setDouble", &ForeFireWasm::setDouble) + .function("getDouble", &ForeFireWasm::getDouble) + .function("setString", &ForeFireWasm::setString) + .function("getString", &ForeFireWasm::getString) + .function("addLayer", &ForeFireWasm::addLayer) + .function("addScalarLayer", &ForeFireWasm::addScalarLayer) + .function("addIndexLayer", &ForeFireWasm::addIndexLayer) + .function("getDoubleArray", &ForeFireWasm::getDoubleArray); +} diff --git a/bindings/wasm/smoke.mjs b/bindings/wasm/smoke.mjs new file mode 100644 index 00000000..0f449568 --- /dev/null +++ b/bindings/wasm/smoke.mjs @@ -0,0 +1,115 @@ +/** + * Smoke test for the WebAssembly build (issue #171). + * + * The JS twin of docs/source/user_guide/python_arrays.rst: a 4 km square + * domain, a uniform fuel map and a wind field, all built as typed arrays, with + * no NetCDF and no files. Run it with: + * + * node bindings/wasm/smoke.mjs path/to/forefire.mjs + */ + +import { pathToFileURL } from "node:url"; +import { resolve } from "node:path"; + +const modulePath = process.argv[2]; +if (!modulePath) { + console.error("usage: node smoke.mjs "); + process.exit(2); +} + +// Resolved against the working directory, the way a path typed on a command +// line is meant to be — not against this file, which would send +// `bin/forefire.mjs` looking inside bindings/wasm/. +const { default: createForeFire } = await import(pathToFileURL(resolve(modulePath)).href); + +const Module = await createForeFire(); +console.log("ForeFire version:", Module.version()); + +const ff = new Module.ForeFire(); + +// Fuel table inline: WindDriven only reads vv_coeff, Kcurv and beta. +ff.setString("fuelsTable", "Index;vv_coeff;Kcurv;beta\n1;1.0;1.0;1.0\n2;0.3;1.0;1.0"); +ff.setDouble("defaultFuelType", 1.0); + +const solver = { + spatialIncrement: 0.5, + minimalPropagativeFrontDepth: 10, + perimeterResolution: 10, + initialFrontDepth: 0.1, + relax: 1.0, + smoothing: 0, + minSpeed: 0.0, + bmapLayer: 1, + windReductionFactor: 1.0, +}; +for (const [key, value] of Object.entries(solver)) ff.setDouble(key, value); +ff.setString("propagationModel", "WindDriven"); + +const L = 4000.0; +console.log(ff.execute(`FireDomain[sw=(0,0,0);ne=(${L},${L},0);t=0]`)); + +const W = 200; +const H = 200; + +// Fuel index 1 everywhere. Shape (t=1, z=1, y=W, x=H), flattened C-order. +const fuel = new Int32Array(W * H).fill(1); + +// Wind layers are (t=1, z=2, y=W, x=H): plane 0 is the u-ward response, plane +// 1 the v-ward one, matching the NumPy example's windU/windV split. +const windU = new Float64Array(2 * W * H); +windU.fill(1.0, 0, W * H); +const windV = new Float64Array(2 * W * H); +windV.fill(1.0, W * H); + +ff.addLayer("propagation", "WindDriven", "propagationModel"); +console.log("fuel layer:", ff.addIndexLayer("table", "fuel", 0, 0, 0, L, L, 0, H, W, 1, 1, fuel)); +console.log("windU layer:", ff.addScalarLayer("windScalDir", "windU", 0, 0, 0, L, L, 0, H, W, 2, 1, windU)); +console.log("windV layer:", ff.addScalarLayer("windScalDir", "windV", 0, 0, 0, L, L, 0, H, W, 2, 1, windV)); + +ff.execute("startFire[loc=(2000,2000,0.);t=0]"); + +let t = 0; +for (let i = 0; i < 8; i++) { + ff.execute(`trigger[wind;loc=(0.,0.,0.);vel=(8,0,0);t=${t}]`); + ff.execute("step[dt=30]"); + t += 30; +} + +const out = ff.execute("print[]"); +console.log(`\nsimulated t = ${ff.getTime()} s`); + +// The print output is the shell's own front format: lines of "x, y, z" triples +// inside FireNode[...] entries. Pull the coordinates out to check the front +// actually moved downwind. +const coords = [...out.matchAll(/loc=\(([-\d.eE+]+),([-\d.eE+]+),/g)].map((m) => [ + parseFloat(m[1]), + parseFloat(m[2]), +]); + +if (coords.length === 0) { + console.error("no front nodes in print[] output:\n" + out.slice(0, 800)); + process.exit(1); +} + +const xs = coords.map((c) => c[0]); +const ys = coords.map((c) => c[1]); +console.log(`front nodes: ${coords.length}`); +console.log( + `x range: ${Math.min(...xs).toFixed(0)}..${Math.max(...xs).toFixed(0)} ` + + `y range: ${Math.min(...ys).toFixed(0)}..${Math.max(...ys).toFixed(0)}` +); + +// The wind blows along +x at 8 m/s for 240 s, so the front should be stretched +// well downwind and only modestly across it. Checking both axes catches a +// transposed layer, which otherwise looks like a perfectly healthy fire that +// happens to run at ninety degrees to the wind. +const spreadX = Math.max(...xs) - Math.min(...xs); +const spreadY = Math.max(...ys) - Math.min(...ys); +if (spreadX < 1000 || spreadX < 2 * spreadY) { + console.error( + `front did not run downwind: x spread ${spreadX.toFixed(0)} m, ` + + `y spread ${spreadY.toFixed(0)} m` + ); + process.exit(1); +} +console.log("\nOK"); From 6b57d34705213673e0eda99d7fef8e8d598d376a Mon Sep 17 00:00:00 2001 From: Hugo <72709893+HugoFara@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:40:54 +0200 Subject: [PATCH 48/63] feat(wasm): run a fire in the browser on real terrain (#211) * feat(wasm): run a fire in the browser on real terrain A static page over the WebAssembly module (#171). landscape.mjs builds the fuel and elevation layers from satellite and Terrarium tiles for any square of ground, worker.mjs runs the whole simulation off the main thread and reports why it ended, and terrain3d.mjs draws the result on a raw WebGL heightfield that tilts up from the 2D map, with click-to-ignite by ray cast. * ci(wasm): publish the browser demo to GitHub Pages on demand * fix(wasm): return addLayer's result, and fail the smoke test on a missing layer addLayer returned void, so JavaScript saw undefined and the demo's worker treated the propagation layer as unregistered: every run failed with "a layer failed to register". The engine side was fixed in #209; this is the binding half, which #210 shipped without. The smoke test printed each layer's result without checking it, which is how this got through CI. It now exits non-zero if any layer is missing. * feat(wasm): split the demo into Explore and Research modes Explore is the default: click the map to light a fire, set the wind on a dial or from a scenario, and watch it play back over a full-height map. Research adds the propagation model, the two tuning factors, the numerics, fuel moisture and bed depths, and the fuel class cut-offs, each labelled with the ForeFire name it sets. Each run now gets its own worker. The engine's state is process-wide (#175), so a reused module kept the first run's propagation model and a model switch did nothing. * fix(wasm): seams, terrain spikes, and zooming out in 3D The tile mosaic was drawn at fractional pixel offsets, so every tile edge was antialiased: a dark seam in the imagery, and in the terrain tiles half-blended pixels that decode to spikes hundreds of metres high. The offsets are now rounded to whole pixels. The wheel in 3D only moved the camera over the ground already loaded. It now zooms the ground as in 2D, and the rebuilt mesh keeps the orbit pose. setView dropped tilt.value, so the second terrain rebuild passed undefined as the camera blend and drew nothing until the view was toggled. * fix(wasm): keep the fire playing through a pan or zoom Any pan or zoom cleared the run, because the fronts were drawn in the current view's metres and would have landed on the wrong ground. Each run now remembers the square it was computed on, its fronts are drawn wherever that square sits in the view, and the ignition is measured against that square rather than a view still waiting on its tiles. * fix(wasm): leave unburnt islands unfilled, and keep the wind arrow off the terrain Each front was filled on its own, so the inner front ForeFire keeps round ground the fire has encircled was filled orange like the burn around it. The fronts are now one shape filled even-odd, which leaves that ground clear. The wind arrow was painted on the 2D canvas, which is also the texture of the 3D mesh, so it was draped over the terrain. It is now an overlay on the map, hidden in 3D where map north turns with the camera. * ci(wasm): publish the demo to GitHub Pages on every push to dev --- .github/workflows/wasm-pages.yml | 96 +++ CHANGELOG.md | 6 + bindings/wasm/README.md | 146 +++- bindings/wasm/forefire_wasm.cpp | 6 +- bindings/wasm/index.html | 1107 ++++++++++++++++++++++++++++++ bindings/wasm/landscape.mjs | 284 ++++++++ bindings/wasm/smoke.mjs | 16 +- bindings/wasm/terrain3d.mjs | 465 +++++++++++++ bindings/wasm/worker.mjs | 238 +++++++ 9 files changed, 2354 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/wasm-pages.yml create mode 100644 bindings/wasm/index.html create mode 100644 bindings/wasm/landscape.mjs create mode 100644 bindings/wasm/terrain3d.mjs create mode 100644 bindings/wasm/worker.mjs diff --git a/.github/workflows/wasm-pages.yml b/.github/workflows/wasm-pages.yml new file mode 100644 index 00000000..34b6f2e1 --- /dev/null +++ b/.github/workflows/wasm-pages.yml @@ -0,0 +1,96 @@ +name: 🌐 Build WASM & Deploy Demo + +# Builds the WebAssembly module (issue #171) and publishes the browser demo to +# GitHub Pages. The .wasm is built here rather than committed: it is a megabyte +# of generated output, and a stale binary next to live sources is worse than a +# two minute build. + +# Publishes from dev. The github-pages environment has to allow dev as a +# deployment branch (Settings → Environments → github-pages). +on: + push: + branches: [ "dev" ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# One deployment at a time; queued runs supersede rather than race. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + # No LFS: the WASM build reads no test data, and emcmake turns the + # integrity check off by default. + + - name: Cache Emscripten SDK + id: emsdk-cache + uses: actions/cache@v4 + with: + path: ~/emsdk + key: emsdk-${{ runner.os }}-6.0.6 + + - name: Install Emscripten + if: steps.emsdk-cache.outputs.cache-hit != 'true' + run: | + # Pinned to the version the demo was developed and tested against. + git clone --depth 1 https://github.com/emscripten-core/emsdk.git ~/emsdk + ~/emsdk/emsdk install 6.0.6 + ~/emsdk/emsdk activate 6.0.6 + + - name: Build the WebAssembly module + run: | + source ~/emsdk/emsdk_env.sh + emcmake cmake -S . -B build-wasm -DCMAKE_BUILD_TYPE=Release + cmake --build build-wasm -j"$(nproc)" + + - name: Report size + run: | + { + echo "### WebAssembly build" + echo "| file | raw | gzip | brotli |" + echo "| --- | --- | --- | --- |" + for f in bin/forefire.wasm bin/forefire.mjs; do + printf '| `%s` | %s | %s | %s |\n' "$(basename "$f")" \ + "$(stat -c%s "$f" | numfmt --to=iec)" \ + "$(gzip -c "$f" | wc -c | numfmt --to=iec)" \ + "$(brotli -c "$f" | wc -c | numfmt --to=iec)" + done + } | tee -a "$GITHUB_STEP_SUMMARY" + + - name: Smoke test under Node + run: node bindings/wasm/smoke.mjs bin/forefire.mjs + + - name: Assemble the site + run: | + mkdir -p _site + cp bin/forefire.mjs bin/forefire.wasm _site/ + cp bindings/wasm/index.html bindings/wasm/worker.mjs \ + bindings/wasm/landscape.mjs bindings/wasm/terrain3d.mjs _site/ + # Pages serves through Jekyll unless told otherwise, which would + # swallow files it does not recognise. + touch _site/.nojekyll + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index f6769e36..7334c651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,12 @@ Merged since v2.5.0, not yet released. typed arrays in place of NumPy arrays. `FOREFIRE_BUILD_CLI` and `FOREFIRE_BUILD_WASM` control the two executables. See `bindings/wasm/README.md`. ([#171]) +- A browser demo of the WebAssembly build (`bindings/wasm/index.html`): pick + a square of ground on a map, and the page builds fuel and elevation layers + from satellite and terrain tiles, runs the fire in a worker, and plays it + back in 2D or on 3D terrain. Explore mode lights a fire where you click; + Research mode exposes the model, its tuning factors, the numerics, and the + fuel moisture and classes. ([#171]) ### Fixed diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index 57eb87c0..f5b03086 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -8,9 +8,10 @@ This is an experimental build, not a supported one yet. It answers the three questions the issue left open: does it link, does it run, how big is it. `.github/workflows/wasm.yml` builds the module and runs `smoke.mjs` on every -pull request. The `.wasm` is built in CI rather than committed: it is a -megabyte of generated output, and a stale binary sitting next to live sources -is worse than a two minute build. +pull request, and `.github/workflows/wasm-pages.yml` publishes the browser +demo to GitHub Pages on every push to `dev`. The `.wasm` is built in CI rather +than committed: it is a megabyte of generated output, and a stale binary +sitting next to live sources is worse than a two minute build. ## What made it possible @@ -55,6 +56,143 @@ Headless, under Node: node bindings/wasm/smoke.mjs bin/forefire.mjs ``` +In a browser — copy the four page files next to the build outputs and serve +the directory, since a `file://` page can neither fetch the `.wasm` nor start a +module worker: + +```sh +cp bindings/wasm/{index.html,worker.mjs,landscape.mjs,terrain3d.mjs} bin/ +python3 -m http.server -d bin +``` + +The solver runs in a worker (`worker.mjs`), which computes the **whole run up +front** — a few hundred milliseconds for a modest fire, a few seconds for one +that burns 800 ha — and the page then scrubs the result on a timeline. Off the +main thread is what keeps the page answering while that happens. + +Each run gets a fresh worker. The engine's state is process-wide (#175), so a +second `FireDomain` in the same module keeps the first one's propagation model, +and switching models between runs would silently do nothing. + +### Explore and Research + +The page has two modes over the same map: + +- **Explore**, the default: click the map to light a fire, pick a wind on the + dial or from a few scenarios, and watch it play back. +- **Research** adds the propagation model, `propagationSpeedAdjustmentFactor` + and `windReductionFactor`, the time step, step count, `perimeterResolution` + and `spatialIncrement`, dead and live fuel moisture and bed depths (the `Md`, + `Ml` and `e` columns of the fuel table), and the excess-green cut-offs + between fuel classes. Each control shows the ForeFire name it sets. + +The models on offer are the ones that run on this fuel table with only slope +and wind: Rothermel, BalbiNov2011Curv, BalbiUnsteady, TroisPourcent and Iso. +Balbi2015, Balbi2020 and BalbiNov2011TMdMl also need moisture and temperature +layers and abort without them; BalbiNov2011 places the front far from the +ignition. + +### The landscape comes from map tiles + +`landscape.mjs` fetches two tiled layers for a square of ground and decodes +both into layer arrays. No NetCDF, no GIS stack: tiles in, an `Int32Array` and +a `Float64Array` out, straight into `addIndexLayer` and `addScalarLayer`. + +The page opens on a 4 km square of the Ajaccio hinterland — the site of +`tests/runff/run.ff` — but the map is live: drag to pan, scroll to resize the +domain between 500 m and 30 km, and click to light a fire. The tile zoom +is chosen from the domain size so the mosaic stays around a thousand pixels and +36 tiles whatever the scale. Panning and zooming keep the run playing: each +run remembers the square it was computed on, and its fronts are drawn wherever +that square now sits on screen. The fetch is debounced, and the canvas previews +the move with the tiles it already has. + +The ignition point is stored as a longitude and latitude rather than as domain +metres, so panning and zooming leave it on the same patch of ground instead of +sliding it across the map. + +**Fuel**, from satellite imagery, classified by excess green `g - (r+b)/2` +into three rows of the repository's own `tests/runff/fuels.csv`: + +| Class | Cut | `fuels.csv` row | Share of the site | +| --- | --- | --- | --- | +| shrub / maquis | ≥ 20 | index 5, bed depth 0.6 m | ~44% | +| sparse / grass | ≥ 10 | index 4, bed depth 0.19 m | ~32% | +| bare, built, water | below | index 0, no fuel bed | ~24% | + +**Elevation**, from Terrarium-encoded SRTM, `r*256 + g + b/256 - 32768` metres. +The site runs from 11 m to 761 m. + +Both are load-bearing, because the default model is **Rothermel** with the +parameters from `tests/runff/params.ff`, and its rate of spread reads the fuel bed and the +slope. Index 0 has no fuel bed, so the fire stops at clearings, tracks and +rooftops. And on a bare slope with no wind at all, ten minutes of spread grows +with the grade while the downhill side stays put: + +| slope | uphill | downhill | +| --- | --- | --- | +| flat | 47 m | 45 m | +| 30% | 99 m | 45 m | +| 60% | 210 m | 43 m | + +> **Layer order matters.** The `altitude` layer must be registered *before* the +> propagation layer. `DataBroker` derives the slope layer at the moment a layer +> named `altitude` is registered; a model that asks for `slope` first gets a +> constant zero-altitude stand-in, and the terrain silently does nothing. + +If the tile hosts are unreachable the page falls back to uniform fuel on flat +ground and says so. + +### Knowing when the fire stops + +A fire that stops does not leave a stationary perimeter behind: ForeFire +discards the front and `print[]` returns nothing at all, which on a canvas is +indistinguishable from a bug. The worker detects the front list emptying, drops +that frame, and reports why it ended — `left-domain` if the last live front was +against the boundary, `burnt-out` otherwise. A slower death, where the +perimeter survives but stops advancing, is caught by a burnt-area plateau over +a twelve-step window (`stalled`), and an ignition on bare ground is reported as +`never-caught` rather than as a fire that stopped. + +### 3D terrain + +`terrain3d.mjs` draws the elevation field as a textured mesh in raw WebGL — no +library, since a heightfield with one directional light is not worth a megabyte +of dependency. The texture is whatever the 2D view painted, so the two views +cannot disagree. Drag to orbit, scroll to zoom the ground in or out (which +fetches new terrain, as in 2D), and the vertical exaggeration is adjustable +because 750 m of relief over 4 km is subtle at true scale. The view degrades to +2D if WebGL is unavailable. + +Switching between the two views is a **camera move, not a swap**. `blendCamera` +interpolates between the orbit and a flat pose — straight down through a +near-orthographic lens (a 0.12 rad field of view from far enough back), framed +so the mesh's unit square exactly fills a square viewport, north up, with the +vertical exaggeration wound to zero. That pose draws what the plain 2D canvas +draws, to within 0.03% of a half-width, so the GL canvas can take over while +still flat and then tilt up: no cross-fade, no second renderer, and no visible +handover. Going back holds the GL canvas until the camera is flat again. + +The flat pose stops a third of a degree short of vertical on purpose — straight +down puts the view direction on the up vector and `lookAt` divides by zero. + +Clicking the terrain lights a fire there, which needs a ray cast rather +than a coordinate transform: `rayFor` rebuilds the world-space ray through a +pixel from the same camera basis `lookAt` uses, and `raycastHeightfield` +marches it until it drops through the surface, then bisects onto the crossing. +It stops at the first ground it meets, so a click cannot land on a slope hidden +behind a ridge. Picking samples the mesh's own vertex heights, not the +full-resolution field, or a click could land somewhere the visible surface is +not. + +`mvpFor`, `blendCamera`, `rayFor`, `raycastHeightfield` and `projectPoint` are +exported so the camera can be checked without a GL context — feed the mesh's corners through and confirm they land in front of +the camera and inside the clip volume. That test exists because the first +version of `multiply` indexed its matrices row-major while `perspective`, +`lookAt` and `uniformMatrix4fv` were all column-major. It still returned a +matrix, and the mesh still drew; it just drew as streaks radiating from a +vanishing point. + ## The API `forefire.mjs` is an ES module exporting a `createForeFire()` factory. It @@ -96,6 +234,8 @@ index with, the same shuffle `_pyforefire.cpp` performs. of the worked example in the NumPy documentation. Run against a native `pyforefire` built from the same tree, the two agree exactly: 349 front nodes, x 1980..3905, y 1763..2308, the front stretched downwind as the doc describes. +- **Speed.** The turning-wind demo runs 15 steps of 20 s over a 4 km domain in + about 1.2 s under Node. ## Known limits diff --git a/bindings/wasm/forefire_wasm.cpp b/bindings/wasm/forefire_wasm.cpp index dc403a1f..58d9a295 100644 --- a/bindings/wasm/forefire_wasm.cpp +++ b/bindings/wasm/forefire_wasm.cpp @@ -105,11 +105,11 @@ class ForeFireWasm { /*! \brief Attach a model-backed layer, e.g. ("propagation", "WindDriven", * "propagationModel"). Distinct from the addLayer *command*, which * builds constant layers instead. */ - void addLayer(const std::string &type, const std::string &name, + bool addLayer(const std::string &type, const std::string &name, const std::string &key) { FireDomain *domain = executor.getDomain(); - if (domain == 0) return; - domain->addLayer(type, name, key); + if (domain == 0) return false; + return domain->addLayer(type, name, key); } /*! \brief Register a continuous field (altitude, wind, moisture, ...). */ diff --git a/bindings/wasm/index.html b/bindings/wasm/index.html new file mode 100644 index 00000000..d2bdbda0 --- /dev/null +++ b/bindings/wasm/index.html @@ -0,0 +1,1107 @@ + + + + + + + ForeFire + + + +
+
+ + +
ForeFirewildfire spread, running in this tab
+ +
+ +
click to light a fire · drag to move · scroll to zoom
+
+
+ + +
—elapsed
+
—burnt
+
+
+
+ + + + + + diff --git a/bindings/wasm/landscape.mjs b/bindings/wasm/landscape.mjs new file mode 100644 index 00000000..6915c848 --- /dev/null +++ b/bindings/wasm/landscape.mjs @@ -0,0 +1,284 @@ +/** + * Builds a ForeFire landscape out of public map tiles, in the browser. + * + * There is no NetCDF here and no GIS stack. Satellite tiles go onto a canvas + * and their pixels are classified into fuel indices; terrain tiles go onto a + * second canvas and decode to metres. The two arrays that come out are exactly + * what addIndexLayer and addScalarLayer take. + * + * Both are load-bearing. Rothermel's rate of spread reads the fuel bed and the + * slope, so the classified imagery stops the fire at clearings and roads, and + * the terrain makes it run uphill: on a bare slope with no wind at all, spread + * grows from 47 m to 210 m over ten minutes between flat ground and a 60% + * grade, while the downhill side stays put. + */ + +const TILE = 256; +const IMAGERY = + "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile"; +// Terrarium-encoded SRTM/NED, elevation = r*256 + g + b/256 - 32768 metres. +const TERRAIN = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium"; + +/*! The three fuel classes, as rows of tests/runff/fuels.csv — the repository's + * own Rothermel table, not invented numbers. Index 0 has a fuel bed depth of + * zero, which is what makes it a barrier. */ +const FUEL_ROWS = { + 5: "5;626.0;600.0;0.1;1.0;4325.0;5844.0;0.6;1.393;0.201;8.3;1.0;300;70000;18802000.0;18802000.0;1800;1000;600;0.3;2.5e-05;4.0;0.3", + 4: "4;613.0;538.0;0.1;1.0;4357.0;6524.0;0.19;1.286;0.085;8.3;1.0;300;70000;18677000.0;18677000.0;1800;1000;600;0.3;2.5e-05;4.0;0.3", + 0: "0;563.0;522.0;0.1;1.0;6099.0;7273.0;0;0.764;0.352;8.3;1.0;300;70000;18169000.0;18167000.0;1800;1000;600;0.3;2.5e-05;4.0;0.3", +}; +const FUEL_HEADER = + "Index;Rhod;Rhol;Md;Ml;sd;sl;e;Sigmad;Sigmal;stoch;RhoA;Ta;Tau0;Deltah;DeltaH;Cp;Cpa;Ti;X0;r00;Blai;me"; + +export const FUELS = [ + { index: 5, label: "shrub / maquis", note: "depth 0.6 m", rgb: [46, 125, 50] }, + { index: 4, label: "sparse / grass", note: "depth 0.19 m", rgb: [190, 180, 70] }, + { index: 0, label: "bare, built, water", note: "no fuel bed", rgb: [110, 100, 95] }, +]; + +export const FUELS_TABLE = [FUEL_HEADER, ...FUELS.map((f) => FUEL_ROWS[f.index])].join("\n"); + +/*! The defaults below are the values in tests/runff/fuels.csv. */ +export const FUEL_DEFAULTS = { deadMoisture: 0.1, liveMoisture: 1.0, depth: { 5: 0.6, 4: 0.19 } }; + +/*! + * The fuel table with dead and live moisture (Md, Ml) and the burnable classes' + * bed depth (e) overridden. Index 0 keeps its zero depth: it is the barrier. + */ +export function fuelsTable({ deadMoisture, liveMoisture, depth }) { + const columns = FUEL_HEADER.split(";"); + const col = (name) => columns.indexOf(name); + const rows = FUELS.map((f) => { + const cells = FUEL_ROWS[f.index].split(";"); + cells[col("Md")] = String(deadMoisture); + cells[col("Ml")] = String(liveMoisture); + if (depth[f.index] !== undefined) cells[col("e")] = String(depth[f.index]); + return cells.join(";"); + }); + return [FUEL_HEADER, ...rows].join("\n"); +} + +/*! Excess-green cut-offs between the classes; see classify(). */ +export const CLASS_DEFAULTS = { shrub: 20, grass: 10 }; + +/*! + * Fuel indices from the per-cell excess green `g - (r+b)/2`. Excess green + * separates vegetation from rock, road and roof about as well as anything this + * cheap can; water lands below both cuts. + */ +export function classify(greenness, { shrub, grass }) { + const fuel = new Int32Array(greenness.length); + const counts = { 5: 0, 4: 0, 0: 0 }; + for (let i = 0; i < greenness.length; i++) { + const g = greenness[i]; + const index = g >= shrub ? 5 : g >= grass ? 4 : 0; + fuel[i] = index; + counts[index]++; + } + return { fuel, counts }; +} + +/*! Web Mercator: fractional pixel coordinates at a given zoom. */ +function project(lon, lat, zoom) { + const n = 2 ** zoom; + const rad = (lat * Math.PI) / 180; + return [ + ((lon + 180) / 360) * n * TILE, + ((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * n * TILE, + ]; +} + +/*! Ground resolution in metres per pixel, which is what ties the tile grid to + * the simulation's Cartesian metres. */ +function resolution(lat, zoom) { + return (156543.03392804097 * Math.cos((lat * Math.PI) / 180)) / 2 ** zoom; +} + +/*! Metres per degree, near enough for panning a few kilometres. */ +export const M_PER_DEG_LAT = 111320; +export const mPerDegLon = (lat) => 111320 * Math.cos((lat * Math.PI) / 180); + +export const SIDE_LIMITS = { min: 500, max: 30000 }; + +/*! + * Pick a tile zoom for a domain of `side` metres. + * + * Chosen so the mosaic stays around a thousand pixels whatever the domain + * size: the tile count per fetch stays roughly constant, and the imagery keeps + * pace with the ground it covers. Capped at 15, past which the terrain tiles + * are upsampled rather than more detailed. + */ +export function zoomFor(lat, side, targetPx = 1024) { + const ideal = Math.log2((156543.03392804097 * Math.cos((lat * Math.PI) / 180) * targetPx) / side); + return Math.max(10, Math.min(15, Math.round(ideal))); +} + +function loadImage(url) { + return new Promise((resolve, reject) => { + const img = new Image(); + // Required to read the pixels back out. Both hosts send + // Access-Control-Allow-Origin, so the canvas stays untainted. + img.crossOrigin = "anonymous"; + img.onload = () => resolve(img); + img.onerror = () => reject(new Error(`tile ${url} failed`)); + img.src = url; + }); +} + +/*! Fetch a tiled layer into one canvas covering the square. `url` differs per + * host: Esri numbers its path z/row/col, the terrain tiles z/x/y. */ +async function mosaic(url, { zoom, left, top, sidePx }) { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = sidePx; + const ctx = canvas.getContext("2d"); + const jobs = []; + for (let ty = Math.floor(top / TILE); ty <= Math.floor((top + sidePx) / TILE); ty++) + for (let tx = Math.floor(left / TILE); tx <= Math.floor((left + sidePx) / TILE); tx++) + jobs.push( + loadImage(url(zoom, tx, ty)).then((img) => + ctx.drawImage(img, tx * TILE - left, ty * TILE - top) + ) + ); + await Promise.all(jobs); + return canvas; +} + +/*! Resample a canvas to the simulation grid and hand back its pixels. */ +function resample(canvas, grid) { + const small = document.createElement("canvas"); + small.width = small.height = grid; + const ctx = small.getContext("2d"); + // Nearest neighbour for the DEM: averaging terrarium's packed bytes across + // pixels is meaningless, since the red channel carries 256 m per unit. + ctx.imageSmoothingEnabled = false; + ctx.drawImage(canvas, 0, 0, grid, grid); + return ctx.getImageData(0, 0, grid, grid).data; +} + +/*! + * Fetch the imagery and terrain covering a `side`-metre square centred on + * (lon, lat), and derive a fuel map and an elevation field from them. + * + * Returns the display canvas plus the two layer arrays, both in the layers' + * (t,z,y,x) order with row 0 at the domain's south edge. Throws if the tiles + * cannot be fetched, so the caller can fall back. + */ +export async function buildLandscape({ lon, lat, side, grid, zoom = zoomFor(lat, side) }) { + const metresPerPx = resolution(lat, zoom); + const sidePx = Math.round(side / metresPerPx); + const [cx, cy] = project(lon, lat, zoom); + // Whole pixels: a tile drawn at a fractional offset is antialiased along + // its edges, which shows as a seam in the imagery and, in the terrain, + // decodes the half-blended edge pixels to spikes hundreds of metres high. + const box = { zoom, left: Math.round(cx - sidePx / 2), top: Math.round(cy - sidePx / 2), sidePx }; + + const [image, terrain] = await Promise.all([ + mosaic((z, x, y) => `${IMAGERY}/${z}/${y}/${x}`, box), + mosaic((z, x, y) => `${TERRAIN}/${z}/${x}/${y}.png`, box), + ]); + + const rgb = resample(image, grid); + const dem = resample(terrain, grid); + + const greenness = new Float32Array(grid * grid); + const altitude = new Float64Array(grid * grid); + let lo = Infinity; + let hi = -Infinity; + + for (let row = 0; row < grid; row++) { + // Image rows run north to south; the layers' row 0 is the domain's + // south edge, so the vertical order flips here. + const y = grid - 1 - row; + for (let x = 0; x < grid; x++) { + const i = (row * grid + x) * 4; + + greenness[x + grid * y] = rgb[i + 1] - (rgb[i] + rgb[i + 2]) / 2; + + const metres = dem[i] * 256 + dem[i + 1] + dem[i + 2] / 256 - 32768; + altitude[x + grid * y] = metres; + if (metres < lo) lo = metres; + if (metres > hi) hi = metres; + } + } + + // Kept alongside the fuel map so the class cut-offs can move without + // fetching the tiles again. + const { fuel, counts } = classify(greenness, CLASS_DEFAULTS); + + return { + image, + greenness, + fuel, + altitude, + nx: grid, + ny: grid, + counts, + metresPerPx, + sidePx, + zoom, + // The square this landscape actually covers, which the page needs to + // keep straight from the square the user is currently looking at. + site: { lon, lat, side }, + elevation: { lo, hi }, + }; +} + +/*! A translucent raster of the fuel classes, drawn at simulation resolution and + * scaled up by the caller. */ +export function fuelOverlay(fuel, grid) { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = grid; + const ctx = canvas.getContext("2d"); + const img = ctx.createImageData(grid, grid); + const palette = Object.fromEntries(FUELS.map((f) => [f.index, f.rgb])); + for (let row = 0; row < grid; row++) { + const y = grid - 1 - row; // back to image order + for (let x = 0; x < grid; x++) { + const [r, g, b] = palette[fuel[x + grid * y]]; + const i = (row * grid + x) * 4; + img.data[i] = r; + img.data[i + 1] = g; + img.data[i + 2] = b; + img.data[i + 3] = 255; + } + } + ctx.putImageData(img, 0, 0); + return canvas; +} + +/*! Relief shading from the elevation field, as a multiply layer over the + * imagery. Satellite pictures taken near local noon carry almost no shadow, + * so without this the terrain is invisible in plan view. */ +export function hillshade(altitude, grid, cellMetres, { azimuth = 315, altitudeAngle = 40 } = {}) { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = grid; + const ctx = canvas.getContext("2d"); + const img = ctx.createImageData(grid, grid); + + const az = ((360 - azimuth + 90) * Math.PI) / 180; + const alt = (altitudeAngle * Math.PI) / 180; + const at = (x, y) => + altitude[Math.min(grid - 1, Math.max(0, x)) + grid * Math.min(grid - 1, Math.max(0, y))]; + + for (let row = 0; row < grid; row++) { + const y = grid - 1 - row; + for (let x = 0; x < grid; x++) { + const dzdx = (at(x + 1, y) - at(x - 1, y)) / (2 * cellMetres); + const dzdy = (at(x, y + 1) - at(x, y - 1)) / (2 * cellMetres); + const slope = Math.atan(Math.hypot(dzdx, dzdy)); + const aspect = Math.atan2(dzdy, -dzdx); + let shade = + Math.cos(alt) * Math.cos(slope) + + Math.sin(alt) * Math.sin(slope) * Math.cos(az - aspect); + shade = Math.max(0, Math.min(1, shade)); + // Rendered as black at varying alpha, to be composited with + // "multiply" — mid-grey means no change, so alpha tracks how far + // the shade falls below neutral. + const i = (row * grid + x) * 4; + img.data[i] = img.data[i + 1] = img.data[i + 2] = 0; + img.data[i + 3] = Math.round(255 * Math.max(0, 0.85 - shade) * 0.9); + } + } + ctx.putImageData(img, 0, 0); + return canvas; +} diff --git a/bindings/wasm/smoke.mjs b/bindings/wasm/smoke.mjs index 0f449568..490c61ab 100644 --- a/bindings/wasm/smoke.mjs +++ b/bindings/wasm/smoke.mjs @@ -61,10 +61,18 @@ windU.fill(1.0, 0, W * H); const windV = new Float64Array(2 * W * H); windV.fill(1.0, W * H); -ff.addLayer("propagation", "WindDriven", "propagationModel"); -console.log("fuel layer:", ff.addIndexLayer("table", "fuel", 0, 0, 0, L, L, 0, H, W, 1, 1, fuel)); -console.log("windU layer:", ff.addScalarLayer("windScalDir", "windU", 0, 0, 0, L, L, 0, H, W, 2, 1, windU)); -console.log("windV layer:", ff.addScalarLayer("windScalDir", "windV", 0, 0, 0, L, L, 0, H, W, 2, 1, windV)); +const layers = { + propagation: ff.addLayer("propagation", "WindDriven", "propagationModel"), + fuel: ff.addIndexLayer("table", "fuel", 0, 0, 0, L, L, 0, H, W, 1, 1, fuel), + windU: ff.addScalarLayer("windScalDir", "windU", 0, 0, 0, L, L, 0, H, W, 2, 1, windU), + windV: ff.addScalarLayer("windScalDir", "windV", 0, 0, 0, L, L, 0, H, W, 2, 1, windV), +}; +console.log("layers:", layers); +const missing = Object.entries(layers).filter(([, ok]) => !ok); +if (missing.length) { + console.error("layers not registered: " + missing.map(([k]) => k).join(", ")); + process.exit(1); +} ff.execute("startFire[loc=(2000,2000,0.);t=0]"); diff --git a/bindings/wasm/terrain3d.mjs b/bindings/wasm/terrain3d.mjs new file mode 100644 index 00000000..f9d8fae7 --- /dev/null +++ b/bindings/wasm/terrain3d.mjs @@ -0,0 +1,465 @@ +/** + * A small WebGL terrain view for the browser demo (issue #171). + * + * The elevation field the simulation runs on is drawn as a mesh, textured with + * whatever the 2D view painted — imagery, fuel, burnt area, fronts. One + * drawing path, two presentations: the map is rendered once to an offscreen + * canvas and uploaded here as a texture, so the two views can never disagree. + * + * Deliberately no library: the whole thing is a textured heightfield with one + * directional light, which is not worth a megabyte of dependency. + */ + +const VERT = ` +attribute vec3 aPos; +attribute vec3 aNormal; +attribute vec2 aUV; +uniform mat4 uMVP; +varying vec2 vUV; +varying float vShade; +void main() { + vUV = aUV; + vec3 sun = normalize(vec3(-0.6, 0.5, 0.62)); + // Lifted off zero so shadowed faces stay legible rather than going black. + vShade = 0.55 + 0.45 * max(dot(normalize(aNormal), sun), 0.0); + gl_Position = uMVP * vec4(aPos, 1.0); +}`; + +const FRAG = ` +precision mediump float; +uniform sampler2D uMap; +varying vec2 vUV; +varying float vShade; +void main() { + vec4 c = texture2D(uMap, vUV); + gl_FragColor = vec4(c.rgb * vShade, 1.0); +}`; + +function compile(gl, type, src) { + const s = gl.createShader(type); + gl.shaderSource(s, src); + gl.compileShader(s); + if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) + throw new Error("shader: " + gl.getShaderInfoLog(s)); + return s; +} + +/*! Column-major, like everything else here and like uniformMatrix4fv with + * transpose=false: element (row r, column c) lives at index c*4 + r. Indexing + * this the other way round still type-checks and still produces a matrix; it + * just projects the mesh into streaks radiating from a vanishing point. */ +function multiply(a, b) { + const o = new Float32Array(16); + for (let c = 0; c < 4; c++) + for (let r = 0; r < 4; r++) { + let sum = 0; + for (let k = 0; k < 4; k++) sum += a[k * 4 + r] * b[c * 4 + k]; + o[c * 4 + r] = sum; + } + return o; +} + +function perspective(fovy, aspect, near, far) { + const f = 1 / Math.tan(fovy / 2); + return new Float32Array([ + f / aspect, 0, 0, 0, + 0, f, 0, 0, + 0, 0, (far + near) / (near - far), -1, + 0, 0, (2 * far * near) / (near - far), 0, + ]); +} + +function lookAt(eye, target, up) { + const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; + const norm = (v) => { + const l = Math.hypot(...v) || 1; + return [v[0] / l, v[1] / l, v[2] / l]; + }; + const cross = (a, b) => [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ]; + const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + const z = norm(sub(eye, target)); + const x = norm(cross(up, z)); + const y = cross(z, x); + return new Float32Array([ + x[0], y[0], z[0], 0, + x[1], y[1], z[1], 0, + x[2], y[2], z[2], 0, + -dot(x, eye), -dot(y, eye), -dot(z, eye), 1, + ]); +} + +const ORBIT_FOV = Math.PI / 4; +// Not π/2: straight down puts the view direction on the up vector and lookAt +// divides by zero. A third of a degree short of vertical reads as flat and +// stays conditioned. +const FLAT_ELEVATION = Math.PI / 2 - 0.005; +// A long lens from far away is orthographic in all but name, which is what +// makes the flat pose match the plain 2D map rather than merely resemble it. +const FLAT_FOV = 0.12; + +const lerp = (a, b, t) => a + (b - a) * t; + +/*! + * Blend between the flat map pose and the orbit pose. + * + * `t` of 0 looks straight down through a near-orthographic lens, framed so the + * mesh's unit square exactly fills a square viewport — pixel for pixel what the + * 2D canvas draws, which is what lets the two views swap unseen. `t` of 1 is + * the orbit the user has dragged to. Everything in between is a camera move, + * so the transition needs no cross-fade and no second renderer. + */ +export function blendCamera({ azimuth, elevation, distance, exaggeration }, t) { + const fov = lerp(FLAT_FOV, ORBIT_FOV, t); + // Half the square subtends half the vertical field of view: the exact fit. + const flatDistance = 0.5 / Math.tan(fov / 2); + return { + // North-up at the flat end, or the map would land rotated. + azimuth: lerp(0, azimuth, t), + elevation: lerp(FLAT_ELEVATION, elevation, t), + distance: lerp(flatDistance, distance, t), + exaggeration: exaggeration * t, + fov, + }; +} + +/*! + * The full model-view-projection for a camera orbiting the origin. + * + * Exported so it can be checked without a GL context: feed it corners of the + * mesh and confirm they land inside the clip volume. That is the test the + * column-major slip above would have failed. + */ +export function mvpFor({ azimuth, elevation, distance, aspect, exaggeration, side, fov = ORBIT_FOV }) { + const eye = [ + distance * Math.cos(elevation) * Math.sin(azimuth), + -distance * Math.cos(elevation) * Math.cos(azimuth), + distance * Math.sin(elevation), + ]; + const view = multiply( + perspective(fov, aspect, 0.02, 40), + lookAt(eye, [0, 0, 0], [0, 0, 1]) + ); + // x and y are a unit square standing for `side` metres, while z is still + // metres; dividing by `side` puts them back in proportion, and the + // exaggeration then lifts the relief to something readable. + const zScale = exaggeration / side; + const model = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, zScale, 0, 0, 0, 0, 1]); + return multiply(view, model); +} + +/*! + * The world-space ray through a point on the screen. + * + * Rebuilt from the camera basis rather than by inverting the projection: the + * basis is the same one `lookAt` builds, so the two cannot drift apart, and + * there is no 4x4 inverse to get subtly wrong. + */ +export function rayFor({ azimuth, elevation, distance, fov = ORBIT_FOV }, ndcX, ndcY, aspect) { + const eye = [ + distance * Math.cos(elevation) * Math.sin(azimuth), + -distance * Math.cos(elevation) * Math.cos(azimuth), + distance * Math.sin(elevation), + ]; + const norm = (v) => { + const l = Math.hypot(...v) || 1; + return [v[0] / l, v[1] / l, v[2] / l]; + }; + const cross = (a, b) => [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ]; + const forward = norm([-eye[0], -eye[1], -eye[2]]); + const right = norm(cross(forward, [0, 0, 1])); + const up = cross(right, forward); + const half = Math.tan(fov / 2); + const dir = norm([ + forward[0] + right[0] * ndcX * half * aspect + up[0] * ndcY * half, + forward[1] + right[1] * ndcX * half * aspect + up[1] * ndcY * half, + forward[2] + right[2] * ndcX * half * aspect + up[2] * ndcY * half, + ]); + return { origin: eye, dir }; +} + +/*! + * March a ray until it drops through a heightfield, and return where. + * + * `heightAt(u, v)` gives the surface in the same units as the ray, over the + * unit square the mesh occupies. Returns normalised `[u, v]` in [0, 1], or + * null if the ray never meets the ground — clicking the sky. + * + * Fixed-step march to bracket the crossing, then bisection to land on it. A + * closed-form solve is not available against an interpolated grid, and a coarse + * march alone would let the point slide down steep faces. + */ +export function raycastHeightfield({ origin, dir }, heightAt, { reach = 6, steps = 2048 } = {}) { + const at = (t) => [origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t]; + const gap = (t) => { + const p = at(t); + const u = p[0] + 0.5; + const v = p[1] + 0.5; + if (u < 0 || u > 1 || v < 0 || v > 1) return null; // off the square + return p[2] - heightAt(u, v); + }; + + const step = reach / steps; + let prevT = null; + let prevGap = null; + for (let i = 0; i <= steps; i++) { + const t = i * step; + const g = gap(t); + if (g === null) { + prevT = null; + prevGap = null; + continue; + } + if (prevGap !== null && prevGap > 0 && g <= 0) { + let lo = prevT; + let hi = t; + for (let k = 0; k < 24; k++) { + const mid = (lo + hi) / 2; + const gm = gap(mid); + if (gm === null) break; + if (gm > 0) lo = mid; + else hi = mid; + } + const p = at((lo + hi) / 2); + const clamp01 = (x) => Math.max(0, Math.min(1, x)); + return [clamp01(p[0] + 0.5), clamp01(p[1] + 0.5)]; + } + prevT = t; + prevGap = g; + } + return null; +} + +/*! Apply a column-major 4x4 to a point, returning normalised device + * coordinates and the clip-space w (positive means in front of the camera). */ +export function projectPoint(m, [x, y, z]) { + const c = [0, 1, 2, 3].map((r) => m[0 + r] * x + m[4 + r] * y + m[8 + r] * z + m[12 + r]); + return { ndc: [c[0] / c[3], c[1] / c[3], c[2] / c[3]], w: c[3] }; +} + +/*! + * Build the terrain view. + * + * `altitude` is the simulation's elevation field in (t,z,y,x) order — row 0 at + * the domain's south edge — `grid` its side, `side` the domain in metres. + * Returns { setMap, render, orbit, zoom, setExaggeration, dispose }, or throws + * if WebGL is unavailable so the caller can stay in 2D. + */ +export function createTerrainView(canvas, { altitude, grid, side, resolution = 129 }) { + const gl = canvas.getContext("webgl", { antialias: true, alpha: false }); + if (!gl) throw new Error("WebGL unavailable"); + + const program = gl.createProgram(); + gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERT)); + gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAG)); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) + throw new Error("link: " + gl.getProgramInfoLog(program)); + + // Sample the elevation field down to the mesh resolution. + const N = Math.min(resolution, grid); + const at = (ix, iy) => { + const x = Math.min(grid - 1, Math.round((ix / (N - 1)) * (grid - 1))); + const y = Math.min(grid - 1, Math.round((iy / (N - 1)) * (grid - 1))); + return altitude[x + grid * y]; + }; + + let lo = Infinity; + let hi = -Infinity; + for (const v of altitude) { + if (v < lo) lo = v; + if (v > hi) hi = v; + } + const mid = (lo + hi) / 2; + + // Positions are normalised to a unit square in x/y so the camera framing is + // independent of the domain size; heights stay in metres, scaled at draw + // time by the exaggeration. + const positions = new Float32Array(N * N * 3); + const uvs = new Float32Array(N * N * 2); + // The same heights the mesh is drawn from, kept for picking: sampling the + // full-resolution field instead would let a click land somewhere the + // visible surface is not. + const meshZ = new Float32Array(N * N); + for (let iy = 0; iy < N; iy++) + for (let ix = 0; ix < N; ix++) { + const i = iy * N + ix; + meshZ[i] = at(ix, iy) - mid; + positions[i * 3] = ix / (N - 1) - 0.5; + positions[i * 3 + 1] = iy / (N - 1) - 0.5; + positions[i * 3 + 2] = meshZ[i]; + uvs[i * 2] = ix / (N - 1); + // The texture is the map as drawn, whose rows run north to south; + // the mesh's y runs south to north, so v flips. + uvs[i * 2 + 1] = 1 - iy / (N - 1); + } + + const indices = []; + for (let iy = 0; iy < N - 1; iy++) + for (let ix = 0; ix < N - 1; ix++) { + const a = iy * N + ix; + indices.push(a, a + 1, a + N, a + 1, a + N + 1, a + N); + } + const indexArray = N * N > 65535 ? new Uint32Array(indices) : new Uint16Array(indices); + const uintExt = N * N > 65535 ? gl.getExtension("OES_element_index_uint") : true; + if (!uintExt) throw new Error("mesh too large for this WebGL context"); + + // Normals are computed once on the CPU: the terrain never changes, and this + // keeps the vertex shader to a single matrix multiply. + const cell = side / (N - 1); + const normals = new Float32Array(N * N * 3); + for (let iy = 0; iy < N; iy++) + for (let ix = 0; ix < N; ix++) { + const i = iy * N + ix; + const dzdx = (at(Math.min(N - 1, ix + 1), iy) - at(Math.max(0, ix - 1), iy)) / (2 * cell); + const dzdy = (at(ix, Math.min(N - 1, iy + 1)) - at(ix, Math.max(0, iy - 1))) / (2 * cell); + const l = Math.hypot(-dzdx, -dzdy, 1); + normals[i * 3] = -dzdx / l; + normals[i * 3 + 1] = -dzdy / l; + normals[i * 3 + 2] = 1 / l; + } + + const buffer = (data, target = gl.ARRAY_BUFFER) => { + const b = gl.createBuffer(); + gl.bindBuffer(target, b); + gl.bufferData(target, data, gl.STATIC_DRAW); + return b; + }; + const posBuf = buffer(positions); + const normBuf = buffer(normals); + const uvBuf = buffer(uvs); + const idxBuf = buffer(indexArray, gl.ELEMENT_ARRAY_BUFFER); + + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + // No mipmaps and clamped edges: the map canvas is not power-of-two. + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + const loc = { + pos: gl.getAttribLocation(program, "aPos"), + normal: gl.getAttribLocation(program, "aNormal"), + uv: gl.getAttribLocation(program, "aUV"), + mvp: gl.getUniformLocation(program, "uMVP"), + map: gl.getUniformLocation(program, "uMap"), + }; + + const camera = { azimuth: -0.6, elevation: 0.72, distance: 1.9 }; + let exaggeration = 2.5; + let transition = 1; // 0 = flat map, 1 = orbit + let mapReady = false; + + gl.enable(gl.DEPTH_TEST); + gl.clearColor(0.05, 0.04, 0.03, 1); + + function setMap(source) { + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source); + mapReady = true; + } + + function render() { + if (!mapReady) return; + const w = canvas.width; + const h = canvas.height; + gl.viewport(0, 0, w, h); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + const posed = blendCamera({ ...camera, exaggeration }, transition); + const final = mvpFor({ ...posed, aspect: w / h, side }); + + gl.useProgram(program); + gl.uniformMatrix4fv(loc.mvp, false, final); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.uniform1i(loc.map, 0); + + const bind = (buf, index, size) => { + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.enableVertexAttribArray(index); + gl.vertexAttribPointer(index, size, gl.FLOAT, false, 0, 0); + }; + bind(posBuf, loc.pos, 3); + bind(normBuf, loc.normal, 3); + bind(uvBuf, loc.uv, 2); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, idxBuf); + gl.drawElements( + gl.TRIANGLES, + indexArray.length, + indexArray.BYTES_PER_ELEMENT === 4 ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT, + 0 + ); + } + + return { + setMap, + render, + /*! The orbit pose, so a page rebuilding the mesh for new ground can + * carry the view over instead of resetting it. */ + camera, + orbit(dAz, dEl) { + camera.azimuth += dAz; + // Stop short of straight down, where the up vector degenerates. + camera.elevation = Math.max(0.08, Math.min(1.45, camera.elevation + dEl)); + }, + zoom(factor) { + // The near limit stops short of the point where the mesh's corners + // all leave the frame and the view loses its footing. + camera.distance = Math.max(0.9, Math.min(5, camera.distance * factor)); + }, + setExaggeration(v) { + exaggeration = v; + }, + /*! 0 = the flat map, 1 = the orbit. Driven by the page's animation. */ + setTransition(t) { + transition = Math.max(0, Math.min(1, t)); + }, + /*! + * Where a click on the canvas meets the ground. + * + * `ndcX`/`ndcY` are normalised device coordinates, -1 to 1 with y up. + * Returns the point in the domain's metres, or null for a click that + * missed the terrain entirely. + */ + pick(ndcX, ndcY, aspect) { + const cam = blendCamera({ ...camera, exaggeration }, transition); + const zScale = cam.exaggeration / side; + // Bilinear over the mesh vertices, which is what the triangles + // interpolate between; nearest would quantise a click to a cell. + const heightAt = (u, v) => { + const gx = u * (N - 1); + const gy = v * (N - 1); + const ix = Math.max(0, Math.min(N - 2, Math.floor(gx))); + const iy = Math.max(0, Math.min(N - 2, Math.floor(gy))); + const fx = gx - ix; + const fy = gy - iy; + const z00 = meshZ[iy * N + ix]; + const z10 = meshZ[iy * N + ix + 1]; + const z01 = meshZ[(iy + 1) * N + ix]; + const z11 = meshZ[(iy + 1) * N + ix + 1]; + return ( + ((z00 * (1 - fx) + z10 * fx) * (1 - fy) + + (z01 * (1 - fx) + z11 * fx) * fy) * + zScale + ); + }; + const hit = raycastHeightfield( + rayFor(cam, ndcX, ndcY, aspect), + heightAt, + { reach: cam.distance + 3 } + ); + return hit ? [hit[0] * side, hit[1] * side] : null; + }, + elevationRange: { lo, hi }, + }; +} diff --git a/bindings/wasm/worker.mjs b/bindings/wasm/worker.mjs new file mode 100644 index 00000000..6a598913 --- /dev/null +++ b/bindings/wasm/worker.mjs @@ -0,0 +1,238 @@ +/** + * Simulation worker for the browser demo (issue #171). + * + * A whole run costs a few hundred milliseconds for a modest fire and a few + * seconds for one that burns eight hundred hectares, so the worker computes + * the entire timeline up front and posts it in one go; the page then scrubs + * through frames rather than waiting on the solver. Running it here rather + * than on the main thread is what keeps the page answering during that. + * + * The fuel map and terrain arrive from the main thread, which is where they + * have to be built: decoding map tiles needs a canvas, and workers have no DOM. + * + * Each worker runs exactly one simulation. The engine's state is process-wide + * (#175): a second FireDomain in the same module keeps the first one's + * propagation model, so changing the model between runs would silently do + * nothing. A fresh worker is a fresh module, and costs a few tens of ms. + */ + +import createForeFire from "./forefire.mjs"; + +let Module = null; + +// The parameters from tests/runff/params.ff. The page overrides the ones its +// research panel exposes. +const SOLVER = { + spatialIncrement: 3, + minimalPropagativeFrontDepth: 20, + perimeterResolution: 10, + initialFrontDepth: 0.1, + relax: 0.5, + smoothing: 0, + minSpeed: 0.009, + bmapLayer: 1, + windReductionFactor: 0.4, + propagationSpeedAdjustmentFactor: 0.6, +}; + +function build(land, model, solver) { + const { side, nx, ny } = land; + const ff = new Module.ForeFire(); + + ff.setString("fuelsTable", land.fuelsTable); + ff.setDouble("defaultFuelType", land.defaultFuel); + for (const [key, value] of Object.entries(solver)) ff.setDouble(key, value); + ff.setString("propagationModel", model); + + ff.execute(`FireDomain[sw=(0,0,0);ne=(${side},${side},0);t=0]`); + + const fuel = land.fuel ?? new Int32Array(nx * ny).fill(land.defaultFuel); + // Two-plane wind layers: plane 0 carries the u response, plane 1 the v. + const windU = new Float64Array(2 * nx * ny); + windU.fill(1, 0, nx * ny); + const windV = new Float64Array(2 * nx * ny); + windV.fill(1, nx * ny); + + const S = side; + // Altitude first, and before the propagation layer: registering a layer + // named "altitude" is what makes the DataBroker derive the slope layer, and + // a propagation model that asks for "slope" before one exists gets a + // constant zero-altitude stand-in that never updates. + if (land.altitude) + ff.addScalarLayer("data", "altitude", 0, 0, 0, S, S, 0, nx, ny, 1, 1, land.altitude); + + const ok = [ + ff.addLayer("propagation", model, "propagationModel"), + ff.addIndexLayer("table", "fuel", 0, 0, 0, S, S, 0, nx, ny, 1, 1, fuel), + ff.addScalarLayer("windScalDir", "windU", 0, 0, 0, S, S, 0, nx, ny, 2, 1, windU), + ff.addScalarLayer("windScalDir", "windV", 0, 0, 0, S, S, 0, nx, ny, 2, 1, windV), + ]; + if (ok.some((v) => !v)) throw new Error("a layer failed to register"); + return ff; +} + +/*! Split print[] into one coordinate array per front. + * + * Fronts nest — an inner front is printed indented inside its parent — so + * every FireFront line starts a fresh polygon. Running them together is what + * draws spurious chords straight across the burn. + */ +function parseFronts(text) { + const fronts = []; + let current = null; + for (const line of text.split("\n")) { + if (line.includes("FireFront[")) { + current = []; + fronts.push(current); + } else if (current && line.includes("FireNode[")) { + const m = /loc=\(([-\d.eE+]+),([-\d.eE+]+),/.exec(line); + if (m) current.push(parseFloat(m[1]), parseFloat(m[2])); + } + } + // A front of one or two nodes is a degenerate sliver, not a perimeter. + return fronts.filter((f) => f.length >= 6).map((f) => Float32Array.from(f)); +} + +/*! Shoelace area, summed over fronts. Inner fronts wind the other way, so + * signed areas cancel and holes are subtracted for free. */ +function burntArea(fronts) { + let total = 0; + for (const f of fronts) { + let a = 0; + for (let i = 0, n = f.length; i < n; i += 2) { + const j = (i + 2) % n; + a += f[i] * f[j + 1] - f[j] * f[i + 1]; + } + total += a / 2; + } + return Math.abs(total); +} + +/*! Did this front run into the edge of the domain? */ +function touchesEdge(fronts, side, margin) { + for (const f of fronts) + for (let i = 0; i < f.length; i += 2) + if ( + f[i] < margin || + f[i] > side - margin || + f[i + 1] < margin || + f[i + 1] > side - margin + ) + return true; + return false; +} + +/*! + * Run the whole timeline and return it, along with how it ended. + * + * A fire that stops does not linger as a stationary perimeter: ForeFire + * discards the front and print[] comes back with nothing at all, which on a + * canvas looks identical to a bug. So the end of the run is detected on the + * front list emptying, the empty frame is dropped, and the cause is reported — + * a front that was touching the domain boundary left the map, anything else + * burnt out. A slower death, where the perimeter survives but stops advancing, + * is caught by the area-plateau test below. + */ +function simulate({ land, model, params, steps, dt, wind, ignition }) { + const solver = { ...SOLVER, ...params }; + const ff = build(land, model, solver); + const [ix, iy] = ignition; + ff.execute(`startFire[loc=(${ix},${iy},0.);t=0]`); + + const frames = []; + const started = performance.now(); + let outcome = { kind: "running" }; + // The plateau test is judged over a window, not step to step: a young + // Rothermel fire adds only a few hundred square metres per step, which is + // indistinguishable from noise one step at a time. + const WINDOW = 12; + + const push = (step, ms) => { + const fronts = parseFronts(ff.execute("print[]")); + frames.push({ + step, + t: ff.getTime(), + ms, + fronts, + nodes: fronts.reduce((n, f) => n + f.length / 2, 0), + area: burntArea(fronts), + windDeg: windAt(step, wind).deg, + }); + }; + push(0, 0); + + for (let step = 1; step <= steps; step++) { + const t0 = performance.now(); + const { vx, vy } = windAt(step, wind); + ff.execute(`trigger[wind;loc=(0.,0.,0.);vel=(${vx.toFixed(4)},${vy.toFixed(4)},0)]`); + ff.execute(`step[dt=${dt}]`); + push(step, performance.now() - t0); + + if (frames.at(-1).fronts.length === 0) { + const last = frames.at(-2); + frames.pop(); // nothing to draw, and it would blank the canvas + outcome = { + kind: + last && touchesEdge(last.fronts, land.side, 3 * solver.perimeterResolution) + ? "left-domain" + : "burnt-out", + t: last ? last.t : 0, + }; + break; + } + + if (frames.length > WINDOW) { + const now = frames.at(-1).area; + const then = frames.at(-1 - WINDOW).area; + // Stalled when the burnt area has stopped growing in any meaningful + // sense — under half a percent over the window, and under one fuel + // cell's worth of ground. + const cell = (land.side / land.nx) ** 2; + if (now - then < Math.max(cell, 0.005 * then)) { + outcome = { kind: "stalled", t: frames.at(-1 - WINDOW).t }; + break; + } + } + if (step % 25 === 0) + self.postMessage({ type: "progress", done: step, total: steps }); + } + + if (outcome.kind === "running") outcome = { kind: "step-limit", t: frames.at(-1).t }; + // An ignition on bare ground never becomes a fire at all, which is worth + // saying plainly rather than reporting as a fire that stopped. + const peak = Math.max(...frames.map((f) => f.area)); + if (outcome.kind === "stalled" && peak < 10 * (land.side / land.nx) ** 2) + outcome = { kind: "never-caught", t: outcome.t }; + + return { + frames, + outcome, + wallMs: performance.now() - started, + domain: land.side, + peakArea: peak, + }; +} + +function windAt(step, wind) { + // Wrapped, or a turning run reports "864°" after an hour of simulated time. + const deg = (wind.turning ? wind.angleDeg + step * wind.turnPerStep : wind.angleDeg) % 360; + const rad = (deg * Math.PI) / 180; + return { vx: wind.speed * Math.cos(rad), vy: wind.speed * Math.sin(rad), deg }; +} + +self.onmessage = ({ data: msg }) => { + if (msg.type === "simulate") { + try { + const result = simulate(msg.run); + self.postMessage( + { type: "timeline", ...result }, + result.frames.flatMap((f) => f.fronts.map((c) => c.buffer)) + ); + } catch (err) { + self.postMessage({ type: "failed", message: String(err && err.message) }); + } + } +}; + +Module = await createForeFire(); +self.postMessage({ type: "ready", version: Module.version() }); From e142544031fe69380b59d1f8cdb76568b657112e Mon Sep 17 00:00:00 2001 From: Hugo <72709893+HugoFara@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:47:32 +0200 Subject: [PATCH 49/63] feat(wasm): colour the burn by arrival time and the front by rate of spread (#213) The faded perimeter trail is replaced by two layers built from what the engine already reports. Arrival paints the burnt ground by when it caught fire, the arrival-time map ForeFire writes natively, with isochrones at a round interval. Spread colours each segment of the front by its nodes' rate of spread, which the worker now keeps from the `vel` in print[] instead of discarding it. Each frame's fronts become one Path2D in domain metres once per run, so the arrival layer redraws through a transform as the view pans and zooms. --- CHANGELOG.md | 2 + bindings/wasm/README.md | 7 ++ bindings/wasm/index.html | 196 ++++++++++++++++++++++++++++++++------- bindings/wasm/worker.mjs | 27 ++++-- 4 files changed, 188 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7334c651..88bd1c31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,8 @@ Merged since v2.5.0, not yet released. back in 2D or on 3D terrain. Explore mode lights a fire where you click; Research mode exposes the model, its tuning factors, the numerics, and the fuel moisture and classes. ([#171]) +- The browser demo colours the burnt area by arrival time, with labelled + isochrones, and the fire front by its rate of spread. ([#171]) ### Fixed diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index f5b03086..b3661e8b 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -86,6 +86,13 @@ The page has two modes over the same map: `Ml` and `e` columns of the fuel table), and the excess-green cut-offs between fuel classes. Each control shows the ForeFire name it sets. +Two layers read the run itself. **Arrival** colours the burnt ground by when +it caught fire, with labelled isochrones at a round interval: the arrival-time +map ForeFire writes natively, rebuilt from the perimeter of every frame. +**Spread** colours the front by each node's rate of spread, the length of the +`vel` that `print[]` reports, scaled to the run's 95th percentile so a few fast +nodes on a tight curve do not wash it out. + The models on offer are the ones that run on this fuel table with only slope and wind: Rothermel, BalbiNov2011Curv, BalbiUnsteady, TroisPourcent and Iso. Balbi2015, Balbi2020 and BalbiNov2011TMdMl also need moisture and temperature diff --git a/bindings/wasm/index.html b/bindings/wasm/index.html index d2bdbda0..f2e73690 100644 --- a/bindings/wasm/index.html +++ b/bindings/wasm/index.html @@ -147,17 +147,28 @@ #outcome.show { display: block; } #outcome b { color: var(--flame); } - #legend { + .keys { position: absolute; top: 0.9rem; right: 1rem; + display: flex; + flex-direction: column; + gap: 0.4rem; + width: 10rem; + pointer-events: none; + } + .keys > div { padding: 0.45rem 0.65rem; border-radius: 6px; background: rgba(13, 11, 10, 0.78); font-size: 12px; display: none; } - body.fuel #legend { display: block; } + body.fuel #legend, + body.ran.arrival #arrivalKey, + body.ran.spread #spreadKey { display: block; } + .scale .bar { height: 8px; border-radius: 2px; margin: 0.25rem 0 0.15rem; } + .scale .ends { display: flex; justify-content: space-between; color: var(--dim); font-size: 11px; } #windBadge { position: absolute; left: 1rem; @@ -288,14 +299,24 @@ } - +
ForeFirewildfire spread, running in this tab
-
+
+
+
+ Arrival
+
0
+
+
+ Spread
+
0
+
+

View

- + +